[Ticket cloned from T21977: PDF Export - format question title]
Hi there
Sometimes the questions wrap in odd places e.g. 6.7 and 6.8 in the image below - is there a way to prevent this?
Thanks in advance.
[Ticket cloned from T21977: PDF Export - format question title]
Hi there
Sometimes the questions wrap in odd places e.g. 6.7 and 6.8 in the image below - is there a way to prevent this?
Thanks in advance.
Hello Miriam,
Unfortunately, the issue is unclear. Please send a problematic demo for research.
Thanks
sorry for the delayed response
in the image you can see that the text does not seem to reach the end of the page the text is from a custom question that we have created called the clause
{ "title": "Individual employment agreement - Casual", "pages": [ { "name": "page7", "title": "Wet weather gear and protective clothing, Requirement for a driver licence and use of the Employer’s vehicles", "description": "\n", "elements": [ { "type": "panel", "name": "panel9", "title": "Wet weather gear and protective clothing", "description": "Workers have the right to take an unpaid meal break for periods of work longer than four hours. A four to six hour day must include one ten minute paid rest break and one 30-minute meal break, work between six and eight hours must include two 10-minute paid rest breaks and one 30-minute meal break, while between a two- to four-hour day it\nmust include one 10-minute rest break. Employers and Employees should agree when breaks are to be taken. If the Employer and Employee cannot agree on the timing of breaks, an Employer must provide breaks according to a prescribed schedule. For further information please consult www.employment.govt.nz .", "innerIndent": 1, "questionStartIndex": "7.1", "elements": [ { "type": "clause", "name": "question51", "title": " Wet weather gear and/or protective clothing will be provided to you by us. Any wet weather gear and/or protective clothing will be returned to us by you at the end of each engagement." }, { "type": "comment", "name": "question52", "title": "Wet weather gear/protective clothing shall be:", "titleLocation": "top" } ] }, { "type": "panel", "name": "panel10", "title": "Requirement for a driver licence and use of the Employer’s vehicles", "innerIndent": 1, "questionStartIndex": "8.1", "elements": [ { "type": "boolean", "name": "question53", "title": "Include a requirement for a driver licence and use of the Employer's vehicles in this agreement?", "titleLocation": "top", "hideNumber": true }, { "type": "clause", "name": "question54", "visibleIf": "{question53} <> false", "title": "It is a condition of your employment that, when required to use motor vehicles or any other machinery which requires a driver licence, that you have at all times the suitable driver licence, and any relevant endorsements, to perform your duties." }, { "type": "clause", "name": "question55", "visibleIf": "{question53} <> false", "title": "In the event that your driver licence is suspended, or that you are disqualified from driving for any period, we are entitled to treat such events as a breach of this Agreement and potentially serious misconduct." }, { "type": "clause", "name": "question56", "visibleIf": "{question53} <> false", "title": "You agree to report any demerit points, suspensions or disqualifications to us immediately." }, { "type": "clause", "name": "question57", "visibleIf": "{question53} <> false", "title": "You must operate all types of vehicles safely and at all times in accordance with legal requirements and our policies (including wearing PPE if applicable)." }, { "type": "clause", "name": "question58", "visibleIf": "{question53} <> false", "title": "You must wear a safety helmet at all times while operating quad bikes or motorbikes." }, { "type": "clause", "name": "question59", "visibleIf": "{question53} <> false", "title": "You must not drive under the influence of drugs or alcohol." } ] } ] } ], "code": "DEA" }
this is the json of the page in the image
import React from "react"; import { ElementFactory, Question, Serializer, SvgRegistry } from "survey-core"; import { localization, PropertyGridEditorCollection } from "survey-creator-core"; import { EmptyBrick, FlatQuestion, FlatRepository, IPoint, IRect, PdfBrick, TextBrick } from "survey-pdf"; import { ReactQuestionFactory, SurveyQuestionElementBase } from "survey-react-ui"; const CUSTOM_TYPE = "clause"; class FlatClause extends FlatQuestion { async generateFlatsContent(point: IRect): Promise<PdfBrick[]> { const rect = { ...point }; rect.yBot = point.yTop; rect.xRight = point.xLeft + 100; return []; } } FlatRepository.register(CUSTOM_TYPE, FlatClause); // Create a question model export class QuestionClauseModel extends Question { getType() { return CUSTOM_TYPE; } } // Register the custom "clause" question type export function registerClause() { ReactQuestionFactory.Instance.registerQuestion(CUSTOM_TYPE, (props) => { return React.createElement(SurveyQuestionClause, props); }); ElementFactory.Instance.registerElement( CUSTOM_TYPE, (name) => { return new QuestionClauseModel(name); }, true ); // Register `clause` as an editor for properties in the Survey Creator's Property Grid PropertyGridEditorCollection.register({ fit: function (prop) { return prop.type === CUSTOM_TYPE; }, getJSON: function () { return { type: CUSTOM_TYPE }; } }); } const locale = localization.getLocale(""); locale.qt[CUSTOM_TYPE] = "Clause"; // Register an SVG icon for the question type SvgRegistry.registerIconFromSvg( CUSTOM_TYPE, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#7c7c7c" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-signature"><path d="m21 17-2.156-1.868A.5.5 0 0 0 18 15.5v.5a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1c0-2.545-3.991-3.97-8.5-4a1 1 0 0 0 0 5c4.153 0 4.745-11.295 5.708-13.5a2.5 2.5 0 1 1 3.31 3.284"/><path d="M3 21h18"/></svg>' ); // Add question type metadata for further serialization into JSON Serializer.addClass( CUSTOM_TYPE, [], function () { return new QuestionClauseModel(""); }, "question" ); // Create a class that renders the clause export class SurveyQuestionClause extends SurveyQuestionElementBase { constructor(props: any) { super(props); } get question() { return this.questionBase; } renderElement() { return null; } } // Remember to call registerClause() somewhere in your application startup
this is the component code
we have an assumption that because the font size has been reduced the text cuts shorter as each brick line is character based are we correct?
Hello Miriam,
Thank you for the update. I may need additional time to create a demo and research this issue. Please stay tuned.
Hello Miriam,

Thank you for your patience. I applied your code, however, I got a different output without those empty spaces on the right side.
View Demo
Please let me know if you manage to reproduce the issue using the demo I shared above. If you require further assistance, please create a problematic sample and send it to us for research.
Thank you
const options = { orientation: "p" as "p", margins: { left: 15, right: 15, top: 10, bot: 10 }, format: [210, 297], matrixRenderAs: "auto" as "auto", fontSize: 11, }; FlatQuestion.CONTENT_GAP_VERT_SCALE = 0.3; FlatSurvey.QUES_GAP_VERT_SCALE = 0.5; FlatSurvey.PANEL_DESC_GAP_SCALE = 0.1; SurveyHelper json.pages.forEach((page: { title: string }) => { if (!page.title) { page.title = " " } }); const pdf = new SurveyPDF(json, options); if (values) { pdf.data = JSON.parse(values ?? '{}'); } console.time("Markdown") pdf.onTextMarkdown.add((_, opt) => { if (opt.text.includes('<')) { opt.html = opt.text; } }) console.timeEnd("Markdown") pdf.mode = "display" console.time("Render Questions") pdf.onRenderQuestion.add(async (_, opt) => { const fontSize = 11; // do the date formatting for pdf if (opt.question.getPropertyValue("inputType") === "date") { const inputBricks = opt.bricks[0].unfold() as Brick[]; const date = opt.question.getPropertyValue("value") inputBricks.forEach((brick) => { if (brick.text === date) { brick.text = formatDate(brick.text) } }) } if (opt.question.getPropertyValue("inputType") === "datetime-local") { const inputBricks = opt.bricks[0].unfold() as Brick[]; const date = opt.question.getPropertyValue("value") if (date !== undefined) { inputBricks.forEach((brick) => { if (brick.text === date) { const [dateString, time] = brick.text.split("T"); brick.text = formatDate(dateString) + " " + twelveHourTime(time); } }) } } //@ts-ignore if (opt.bricks[0].bricks) { //@ts-ignore const titleBricks = opt.bricks[0]?.bricks[0]?.unfold() as Brick[] ?? []; //titleBricks.forEach((brick) => { for (let i = 0; i < titleBricks.length; i++) { const b = titleBricks[i] as Brick; if ("html" in b) { b.fontSize = fontSize; b.html = setStylesForFromHtmlBrick(b.html, fontSize, undefined, true); } } } }); console.timeEnd("Render Questions") console.time("Render Panel") pdf.onRenderPanel.add((_, opt) => { const fontsize = 11 const numberShown = opt.panel.showNumber; if (numberShown) { //@ts-ignore const numberBrick = opt.bricks[0].bricks[0].bricks[0] numberBrick.fontSize = fontsize; } //@ts-ignore const titleBricks = numberShown ? opt.bricks[0].bricks[1].unfold() : opt.bricks[0].bricks[0].unfold(); //@ts-ignore const titleBricksCount = opt.bricks[0].bricks.length; //@ts-ignore const rowLineBrick = opt.bricks[0].bricks[titleBricksCount - 1]; const parentIsPanel = opt.panel.parent.getType() == "panel"; titleBricks.forEach((brick: Brick) => { if (!parentIsPanel) { rowLineBrick.color = "#000" } if ("text" in brick) { brick.fontSize = fontsize; } if ("html" in brick) { brick.fontSize = fontsize; brick.html = setStylesForFromHtmlBrick(brick.html, fontsize, undefined, true); } }) }) console.timeEnd("Render Panel") console.time("Render Page") pdf.onRenderPage.add((_, opt) => { //@ts-ignore const title = opt.page.jsonObj.title; const bricks = opt.bricks[0].unfold(); bricks.forEach((brick) => { const b = brick as PdfBrick & { text: string, html: string, textColor: string }; if ("text" in b && title.includes(b.text)) { b.textColor = "#339933"; b.fontSize = 16; } if ("html" in b) { b.html = setStylesForFromHtmlBrick(b.html, 16, "#339933"); } }); }) console.timeEnd("Render Page") console.time("Render Footer") pdf.onRenderFooter.add((_, canvas) => { if (canvas.pageNumber % 2 == 0) { canvas.drawText({ text: `${party?.[1]?.firstName ?? "TBD"} ${party?.[1]?.lastName ?? "TBD"} | ${title} | Page ${canvas.pageNumber}`, horizontalAlign: HorizontalAlign.Right, verticalAlign: VerticalAlign.Bottom, margins: { bot: 15, right: 15 }, fontSize: 8 }); canvas.drawText({ text: `${formatDate(new Date())}`, horizontalAlign: HorizontalAlign.Left, verticalAlign: VerticalAlign.Bottom, margins: { bot: 15, left: 15 }, fontSize: 8 }) } else { canvas.drawText({ text: "Page " + canvas.pageNumber`, horizontalAlign: HorizontalAlign.Left, verticalAlign: VerticalAlign.Bottom, margins: { bot: 15, left: 15 }, fontSize: 8 }); canvas.drawText({ text: `${formatDate(new Date())}`, horizontalAlign: HorizontalAlign.Right, verticalAlign: VerticalAlign.Bottom, margins: { bot: 15, right: 15 }, fontSize: 8 }); } }); console.timeEnd("Render Footer") pdf.locale = 'en';
this is the configuration for the pdf
we also do some monkey patching to remove boldness and set default font sizes based on bricks
this is done at the top level
TextBrick.prototype.renderInteractive = async function () { let alignPoint: IPoint = this.alignPoint(this); let oldFontSize: number = this.controller.fontSize; switch (Object.getPrototypeOf(this).constructor.name) { case "TextBoldBrick2": this.controller.fontSize = 11; break; case "TextBrick2": //don't overwrite footer sizes if (this.question) { this.controller.fontSize = 11; } else { this.controller.fontSize = this.fontSize; } break; default: this.controller.fontSize = this.fontSize; break; } let oldTextColor: string = this.controller.doc.getTextColor(); this.controller.doc.setTextColor(this.text === "*" ? '#ff0000' : this.textColor); this.controller.doc.text(this.text, alignPoint.xLeft, alignPoint.yTop, this.align); this.controller.doc.setTextColor(oldTextColor); this.controller.fontSize = oldFontSize; };
Hi,
Thank you for sharing the code. I may need additional time to reply. Please stay tuned.
Miriam,
I tried the code, however, it seems that some component code is missing (e.g.,
TextBoldBrick2
). For me to move forward and research the issue, I would appreciate it if you share a full runnable demo so that I can run it on my end.Thank you
what line brakes for you? textboldbrick2 is a surveyjs thing all the bricktypes are [bricktype]2 eg htmlbrick is htmlbrick2 hence the switch cases
Hello,
Thank you for the update. The following functions are not recognized by the compiler:
formatDate
twelveHourTime
setStylesForFromHtmlBrick
Are those your custom functions?
The following code was not properly recognized:
const b = titleBricks[i] as Brick;
text: `${party?.[1]?.firstName ?? "TBD"} ${party?.[1]?.lastName ?? "TBD"} | ${title} | Page ${canvas.pageNumber}
To help us communicate more quickly and efficiently, and find the cause of those empty spaces which appear on your end, I would appreciate it if you modify the attached demo to insert your custom functions and update the demo to illustrate the issue.
Thank you for your cooperation. I look forward to your reply.
attached you will see the following custom functions
export function formatDate(time: any, format: Intl.LocalesArgument = 'en-NZ', options: Intl.DateTimeFormatOptions = {}): string { const timeAsDate = ToDate(time); if (!timeAsDate) return 'N/A'; const optionsConflictWithDateStyle = Object.keys(options).some(option => ['weekday', 'era', 'year', 'month', 'day', 'hour', 'minute', 'second', 'timeZoneName'].includes(option) ); const defaultOptions = optionsConflictWithDateStyle ? {} : { dateStyle: 'short' as const }; const formatter = new Intl.DateTimeFormat(format, { ...defaultOptions, ...options }); return formatter.format(timeAsDate); } export function twelveHourTime(time: string) { const [hour, min] = time.split(':'); const hourNum = Number(hour); const newHour = hourNum % 12 || 12; const period = hourNum >= 12 ? 'pm' : 'am'; return `${newHour}:${min} ${period}`; } function setStylesForFromHtmlBrick(htmlstring: string, fontSize: number, color?: string, removeBold: boolean = false) { const tempDiv = document.createElement("div"); tempDiv.innerHTML = htmlstring; const target = tempDiv.querySelector("div.__surveypdf_html") as HTMLElement; if (target) { const textNodes = Array.from(target.childNodes).filter(node => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== ''); const visibleText = textNodes.map(node => node?.textContent?.trim()).join(' '); target.style.fontSize = `${fontSize}pt`; if (visibleText === "*") { target.style.color = 'red'; } else { target.style.color = color ?? target.style.color; } if (removeBold) { target.style.fontWeight = 'normal'; } } const styleTag = tempDiv.querySelector('style'); if (styleTag) { //remove line-height styleTag.innerHTML = styleTag.innerHTML.replace(/line-height:\s?[\d.]+pt\s?;?/g, ''); // set margin styleTag.innerHTML = styleTag.innerHTML.replace(/margin:\s?0\s?;?/g, 'margin: 1.5'); } return tempDiv.innerHTML; } export type Brick = PdfBrick & { question: any, text: string, html: string, bricks: Brick[] };
you can ignore the footers entirely they arnt what are causing the problems
send this to the developer and return it to you :)
Attached is the updated file with the missing functions if you need anything further please feel free to let me know
Hello Miriam,
Thank you for sharing the demo. I'll research it and update you shortly.
Miriam,

Thank you for your patience. I tested your demo on my end.
A survey appears as follows:
After the survey is exported to PDF, a PDF document appears as follows:

I would appreciate it if you point out issues which you'd like to fix in your PDF file.
Thank you