ReportLab LayoutError: too large on page

12.4k views Asked by At

I'm doing my first program using ReportLab where I don't know in advance where the page breaks will fall and I'm having trouble. To keep things simple I'm using the SimpleDocTemplate. My flowables look something like this:

flowables = [Paragraph("Some title", style=headerParagraphStyle),
             Spacer(0, 10),
             Paragraph("first paragraph", style=bodyParagraphStyle),
             Paragraph("second paragraph", style=bodyParagraphStyle),
             ...
             Paragraph("nth paragraph", style=bodyParagraphStyle),
             PageBreak(),
             Paragraph("Some title", style=headerParagraphStyle),
             Spacer(0, 10),
             Paragraph("first paragraph", style=bodyParagraphStyle),
             Paragraph("second paragraph", style=bodyParagraphStyle),
             ...
             Paragraph("mth paragraph", style=bodyParagraphStyle),
             PageBreak(),
             ...]

When I build my PDF everything goes fine so long as my n or m or however many body paragraphs fit on one page, but if they run over, I get an error like the following:

reportlab.platypus.doctemplate.LayoutError: Flowable <Paragraph at 0xb79800 frame=normal>20th paragraph: too large on page 3

Can't seem to find a good reason for why this keeps happening to me. Any suggestions? It does this even if I remove the PageBreaks(). All of the paragraphs are relatively short, mostly less than one sentence/line across.

ETA: I'm posting all of the code (with minor identifiers stripped) that is generating the error for me. I've converted it to read a CSV file, so I've posted its contents, too. The exact error that this code generates for me when I run it is:

Traceback (most recent call last):
  File "./spice_dev.py", line 355, in 
    departmentReportDoc.build(eachDepartment.report, onFirstPage=onReportPage, onLaterPages=onReportPage)
  File "/usr/local/lib/python2.6/site-packages/reportlab/platypus/doctemplate.py", line 1010, in build
    BaseDocTemplate.build(self,flowables, canvasmaker=canvasmaker)
  File "/usr/local/lib/python2.6/site-packages/reportlab/platypus/doctemplate.py", line 777, in build
    self.handle_flowable(flowables)
  File "/usr/local/lib/python2.6/site-packages/reportlab/platypus/doctemplate.py", line 694, in handle_flowable
    raise LayoutError(ident)
reportlab.platypus.doctemplate.LayoutError: Flowable Additional comments and suggestions for improvement: too large on page 3

Some debugging work shows that the error is a result of this paragraph (although it can be other flowables, depending on content length) trying to split, being postponed, and then still not able to be split after the call to handle_frameEnd(). Suggestions?

spice.py:

#!/usr/local/bin/python
# vim: set fileencoding=latin-1

#import csv
import os
import os.path
import time

import numpy
import scipy.stats._support

#for debugging
import sys
import traceback

# imports for reportlab

from reportlab.platypus import *
from reportlab.lib.styles import ParagraphStyle
from reportlab.lib.pagesizes import LETTER, landscape, portrait
from reportlab.lib.units import inch
from reportlab.lib import colors
from reportlab.pdfgen import canvas
from reportlab.lib.enums import *

##########################
####### Constants ########
##########################

kNumberOfQuestions = 4

##########################
####### Reportlab ########
##########################

def _doNothing(canv, doc):
    pass

headerParagraphStyle = ParagraphStyle("Header", fontName="Helvetica-Bold", fontSize=16, spaceAfter = .05*inch, alignment=TA_CENTER)
header2ParagraphStyle = ParagraphStyle("Header2", fontName="Helvetica-Bold", fontSize=14, spaceAfter = .1*inch, spaceBefore=.5*inch, alignment=TA_CENTER)
subheaderInfoParagraphStyle = ParagraphStyle("Subheader Info", fontName="Helvetica-Bold", fontSize=10, alignment=TA_CENTER)
questionParagraphStyle = ParagraphStyle("Question header", fontName="Helvetica-Bold", fontSize = 10, alignment=TA_LEFT)
commentParagraphStyle = ParagraphStyle("Comment", fontName="Helvetica", fontSize = 10, alignment=TA_LEFT)
instructorParagraphStyle = ParagraphStyle("Instructor Header", fontName="Helvetica", fontSize=10, alignment=TA_LEFT)
basicTableStyle = TableStyle(
        [('FONT', (0, 0), (-1, 0), 'Helvetica-Bold', 9),
         ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
         ('LINEBELOW', (0, 0), (-1, 0), 1, colors.black),
         ('INNERGRID', (0, 0), (-1, 0), 1, colors.black),
         ('FONT', (0, 1), (-1, -1), 'Helvetica', 9),
         ('ALIGN', (0, 1), (-1, 1), 'LEFT'),
         ('ALIGN', (1, 1), (-1, -1), 'CENTER'),
         ('LINEAFTER', (0, 1), (0, -1), 1, colors.black),
         ('LINEBEFORE', (1, 1), (-1, -1), 1, colors.black),
         ('ROWBACKGROUNDS', (0, 1), (-1, -1), (colors.white, (.9, .9, .9))),
         ('TOPPADDING', (1, 1), (-1, -1), 8)
        ])
statTableStyle = TableStyle(
        [('FONT', (0, 0), (0, -1), 'Helvetica-Bold', 9),
         ('ALIGN', (0, 0), (0, -1), 'RIGHT'),
         ('INNERGRID', (0, 0), (-1, -1), 1, colors.black),
         ('FONT', (1, 0), (1, -1), 'Helvetica', 9),
         ('ALIGN', (1, 0), (1, -1), 'LEFT')
        ])
headerTableStyle = TableStyle(
        [('FONT', (0, 0), (-1, 0), 'Helvetica', 8),
         ('FONT', (0, 1), (-1, 1), 'Helvetica-Bold', 8),
         ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
         ('LINEBELOW', (0, 0), (-1, 0), 1, colors.black),
         ('LINEAFTER', (0, 0), (-1, -1), 20, colors.white),
         ('LEFTPADDING', (0,0), (-1, -1), 15),
         ('RIGHTPADDING', (0, 0), (-1, -1), 15)
        ])

##########################
######## Classes #########
##########################

class Course:
    def __init__(self):
        self.prefix = ""
        self.number = ""
        self.section = ""
        self.instructor = ""
        self.email = ""
        self.name = "None Found"
        self.enrollment = 0
        self.semester = ""
        self.report = [] # subreport for the course
        self.dataFile = []

    def __repr__(self):
        return self.prefix + " " + self.number + " " + self.section + " " + self.instructor

class Instructor:
    def __init__(self):
        self.name = ""
        self.email = ""
        self.courses = dict([])
        self.report = []
        self.dataFile = []

    def __repr__(self):
        return self.name + " " + self.email

class Department:
    def __init__(self):
        self.name = ""
        self.instructors = dict([]) #contains Instructor objects
        self.report = [] #subreport for the department
        self.dataFile = []

    def __repr__(self):
        return self.name

class College:
    def __init__(self):
        self.name = ""
        self.departments = dict([]) #contains Department objects
        self.report = [] #subreport for the college
        self.dataFile = []

    def __repr__(self):
        return self.name + ":" + `self.departments`

class University:
    def __init__(self):
        self.name = ""
        self.colleges = dict([]) #contains College objects

    def __repr__(self):
        return self.name + ":" + `self.colleges`

#########################
### Utility Functions ###
#########################

def onReportPage(canv, doc):
    #display general info at the top of every page
    canv.setFont('Courier', 10)
    canv.drawString(inch, 10.5*inch, "Wassamata U")
    canv.drawString(inch, 10.35*inch, "Student Comments")

    canv.drawString(5.75*inch, 10.5*inch, "Year/Term:  " + uSemesterYear + "/" + uSemesterTerm)
    canv.drawString(5.75*inch, 10.35*inch, " Semester:  " + `semesterNumber`)

def xmlify(text):
    return text.replace('&','&amp;').replace('<','&lt;').replace('>','&gt;')

def semesterDescription(semesterNumber):
    # reference semester is Spring 1964, i.e. semester 0
    semesterNumber = semesterNumber / 10
    year = `1964 + (semesterNumber / 3)`
    term = "Spring"
    if semesterNumber % 3 == 1:
        term = "Summer"
    elif semesterNumber % 3 == 2:
        term = "Fall"

    return year, term

#########################
######### Main ##########
#########################

# set up some options
# should have a ui later

# ugly, but quick
# used to get them on the top of every page
global uSemesterYear
global uSemesterTerm

print "\n"

university = University()
university.name = "Wassamata U"

commentsFile = open("spireport2.csv", "rb").read()
commentRecords = commentsFile.split("ô\r\n")
commentsArray = []
for commentRecord in commentRecords:
    commentsArray.append(commentRecord.split("æ"))
commentsArray.pop()

print "Reading in the SPI file"
#read in data from file
for row in commentsArray:
    #print row
    # college
    currentCollege = university.colleges.get(row[0])
    if currentCollege == None:
        currentCollege = College()
        currentCollege.name = row[0].replace("/", " ")
        university.colleges[row[0]] = currentCollege

    #department
    currentDepartment = currentCollege.departments.get(row[1])
    if currentDepartment == None:
        currentDepartment = Department()
        currentDepartment.name = row[1].replace("/", " ")
        currentCollege.departments[row[1]] = currentDepartment

    #instructor
    currentInstructor = currentDepartment.instructors.get(row[2] + row[3])
    if currentInstructor == None:
        currentInstructor = Instructor()
        currentInstructor.name = row[3].replace("/", " ")
        currentInstructor.email = row[2].replace("/", " ")
        currentDepartment.instructors[row[2] + row[3]] = currentInstructor

    #course
    currentCourse = currentInstructor.courses.get(row[5] + row[6] + row[7])
    if currentCourse == None:
        currentCourse = Course()
        currentCourse.prefix = row[5][:3]
        currentCourse.number = row[5][3:]
        currentCourse.section = row[6]
        currentCourse.instructor = row[3]
        currentCourse.email = row[2]
        currentCourse.name = row[4]
        currentCourse.semester = row[7]
        currentCourse.enrollment = int(row[8])
        currentInstructor.courses[row[5] + row[6] + row[7]] = currentCourse

    data = row[9:9+kNumberOfQuestions]

    currentCollege.dataFile.append(data) #split the data file by college for later
    currentInstructor.dataFile.append(data)
    currentDepartment.dataFile.append(data)
    currentCourse.dataFile.append(data)

semesterNumber = int(university.colleges.values()[0].departments.values()[0].instructors.values()[0].courses.values()[0].semester)
uSemesterYear, uSemesterTerm = semesterDescription(semesterNumber)

reportDocContent = []

print "Processing the SPI comments"

for eachCollege in university.colleges.values():
    print "\tProcessing " + eachCollege.name
    collegeReportStartingIndex = len(reportDocContent)

    reportDocContent.append(Spacer(0, 100))
    reportDocContent.append(Paragraph("Student comments for " + eachCollege.name, headerParagraphStyle))
    reportDocContent.append(PageBreak())

    for eachDepartment in eachCollege.departments.values():
        print "\t\tProcessing " + eachDepartment.name
        departmentReportStartingIndex = len(reportDocContent)

        reportDocContent.append(Spacer(0, 100))
        reportDocContent.append(Paragraph("Student comments for " + eachDepartment.name, headerParagraphStyle))
        reportDocContent.append(PageBreak())

        for eachInstructor in eachDepartment.instructors.values():
            print "\t\t\tProcessing " + eachInstructor.name
            instructorReportStartingIndex = len(reportDocContent)

            reportDocContent.append(Spacer(0, 100))
            reportDocContent.append(Paragraph("Student comments for " + eachInstructor.name + ", " + eachInstructor.email, headerParagraphStyle))
            reportDocContent.append(PageBreak())

            for eachCourse in eachInstructor.courses.values():
                courseReportStartingIndex = len(reportDocContent)

                reportDocContent.append(Paragraph("<para leftIndent=54><b>Instructor Name:</b>  " + eachCourse.instructor + "</para>", instructorParagraphStyle))
                reportDocContent.append(Spacer(0, 10))

                headerTableContent1 = [[eachDepartment.name + "/" + eachCollege.name, eachCourse.prefix + eachCourse.number + eachCourse.section, eachCourse.name], ["Department/School", "Course-Section Number", "Course Name"]] 
                headerTableContent2 = [[eachCourse.enrollment if eachCourse.enrollment > 0 else "Unknown",
                                        len(eachCourse.dataFile),
                                        ("%.2f" % (float(len(eachCourse.dataFile))/eachCourse.enrollment*100) if eachCourse.enrollment != 0 else "0.00")],
                                       ["Number of Students Enrolled", "Number Responding", "% of Response"]]
                reportDocContent.append(Table(headerTableContent1, style=headerTableStyle))
                reportDocContent.append(Spacer(0, 10))
                reportDocContent.append(Table(headerTableContent2, style=headerTableStyle))

                i = 0
                for i in range(0, kNumberOfQuestions):
                    question = ""
                    if i == 0:
                        question = "The thing(s) I like the MOST about this course:"
                    elif i == 1:
                        question = "The thing(s) I like the LEAST about this course:"
                    elif i == 2:
                        question = "What is your reaction to the method of evaluating your mastery of the course (i.e., testing, grading, out of class assignments (term papers), instructor feedback, etc.):"
                    elif i == 3:
                        question = "Additional comments and suggestions for improvement:"

                    reportDocContent.append(Spacer(0, 10))
                    reportDocContent.append(Paragraph(question, style=questionParagraphStyle))
                    reportDocContent.append(Spacer(0, 5))

                    commentParagraph = ""
                    for comments in eachCourse.dataFile:
                        if comments[i] != "":
                            commentParagraph += unicode(comments[i], 'latin-1') + "<br/>"
                    reportDocContent.append(Paragraph(commentParagraph, style=commentParagraphStyle))

                eachCourse.report = reportDocContent[courseReportStartingIndex:]
                reportDocContent.append(PageBreak())

            eachInstructor.report = reportDocContent[instructorReportStartingIndex:]

        eachDepartment.report = reportDocContent[departmentReportStartingIndex:]

    eachCollege.report = reportDocContent[collegeReportStartingIndex:]

# build directory structure to put reports in
for eachCollege in university.colleges.values():
    if (not os.path.exists(eachCollege.name)):
        os.mkdir(eachCollege.name)
    for eachDepartment in eachCollege.departments.values():
        if (not os.path.exists(os.path.join(eachCollege.name, eachDepartment.name))):
            os.mkdir(os.path.join(eachCollege.name, eachDepartment.name))
        for eachInstructor in eachDepartment.instructors.values():
            if (not os.path.exists(os.path.join(eachCollege.name, eachDepartment.name, eachInstructor.name + " - " + eachInstructor.email))):
                os.mkdir(os.path.join(eachCollege.name, eachDepartment.name, eachInstructor.name + " - " + eachInstructor.email))

print "Building Comments Report PDFs"
for eachCollege in university.colleges.values():
    print "\tBuilding Comments Report for " + eachCollege.name

    collegeReportDoc = SimpleDocTemplate(os.path.join(eachCollege.name, eachCollege.name + " SPI Comments Report.pdf"), pagesize=portrait(LETTER), allowSplitting=1)
    collegeReportDoc.leftMargin = .25*inch
    collegeReportDoc.rightMargin = .25*inch
    collegeReportDoc.bottomMargin = .25*inch

    collegeReportDoc.build(eachCollege.report, onFirstPage=onReportPage, onLaterPages=onReportPage)

    for eachDepartment in eachCollege.departments.values():
        print "\t\tBuilding Comments Report for " + eachDepartment.name

        departmentReportDoc = SimpleDocTemplate(os.path.join(eachCollege.name, eachDepartment.name, eachDepartment.name + " SPI Comments Report.pdf"), pagesize=portrait(LETTER), allowSplitting=1)
        departmentReportDoc.leftMargin = .25*inch
        departmentReportDoc.rightMargin = .25*inch
        departmentReportDoc.bottomMargin = .25*inch

#       import pdb; pdb.set_trace()
        departmentReportDoc.build(eachDepartment.report, onFirstPage=onReportPage, onLaterPages=onReportPage)

        print "\t\t\tBuilding Comments Reports for individual instructors"
        for eachInstructor in eachDepartment.instructors.values():
            instructorReportDoc = SimpleDocTemplate(os.path.join(eachCollege.name, eachDepartment.name, eachInstructor.name + " - " + eachInstructor.email, eachInstructor.name + " SPI Comments Report.pdf"), pagesize=portrait(LETTER), allowSplitting=1)
            instructorReportDoc.leftMargin = .25*inch
            instructorReportDoc.rightMargin = .25*inch
            instructorReportDoc.bottomMargin = .25*inch

            instructorReportDoc.build(eachInstructor.report, onFirstPage=onReportPage, onLaterPages=onReportPage)
#we do this one last because it's the biggest; otherwise it'd be at the beginning of the pdf report generation process
print "\tBuilding SPI Report for University"
reportDoc = SimpleDocTemplate("SPI Comments Report.pdf", pagesize=portrait(LETTER), allowSplitting=1)
reportDoc.leftMargin = .25*inch
reportDoc.rightMargin = .25*inch
reportDoc.bottomMargin = .25*inch

reportDoc.build(reportDocContent, onFirstPage=onReportPage, onLaterPages=onReportPage)

spireport2.csv:

College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æsample commentsææi like itæô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205æone with a line break; let's see what happensæææweird charactèrs  áÈ-{ô
College of Health & Public AffæCriminal Justice/Legal Studiesæ[email protected]æAn InstructoræFOUNDATIONS OF LAW ENFORCEMENTæCJE5021æ0001æ1370æ   7ææææô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205ææææô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205ææææô
College of Health & Public AffæHealth Info Mgmtæ[email protected]æAn InstructoræMEDICAL TERMINOLOGYæHSC3537æ0M01æ1370æ 205ææææô
College of Arts & HumanitiesæEnglishæ[email protected]æAn InstructoræHARLEM, HAITI, AND HAVANAæAML3615æ0001æ1370æ  35ææææô
College of Arts & HumanitiesæEnglishæ[email protected]æAn InstructoræCONT AMERICAN WOMEN S FICTIONæAML3283æ0001æ1370æ  35ææææô
College of Arts & HumanitiesæEnglishæ[email protected]æAn InstructoræPOST-WORLD WAR II FICTIONæLIT4303æ0M01æ1370æ  32ææææô
College of Arts & HumanitiesæEnglishæ[email protected]æAn InstructoræMAJOR AMERICAN AUTHORSæAML4300æ0001æ1370æ  33ææææô
College of Arts & HumanitiesæEnglishæ[email protected]æAn InstructoræPRACTICAL CRITICISMæENG3010æ0001æ1370æ  36ææææô
2

There are 2 answers

3
Gordon Seidoh Worley On BEST ANSWER

Found a workaround myself. Based on some code changes made in rst2pdf, I was able to get my code working reliably. The solution, although ugly, is to wrap all my Paragraphs in the section of the code that creates Paragraphs for the student comments inside KeepTogether. For example, I changed one relevant line to:

reportDocContent.append(KeepTogether(Paragraph(commentParagraph, style=commentParagraphStyle)))

and now it works (at least, I haven't been able to break it yet after trying lots of sample data).

1
yoshi On

Looks like this is an old question but I experienced this same error very recently, and came here to find some advices. The fix I made was to make the spacer size a half of the old size. It was previously Spacer(1,0.2*inch) and I changed it to Spacer(1,0.1*inch). Then there are no more such errors.

FYI the error message I got was

  File "/opt/python2.7/lib/python2.7/site-packages/reportlab-2.5-py2.7-linux-x86_64.egg/reportlab/platypus/doctemplate.py", line 1117, in build
    BaseDocTemplate.build(self,flowables, canvasmaker=canvasmaker)
  File "/opt/python2.7/lib/python2.7/site-packages/reportlab-2.5-py2.7-linux-x86_64.egg/reportlab/platypus/doctemplate.py", line 880, in build
    self.handle_flowable(flowables)
  File "/opt/python2.7/lib/python2.7/site-packages/reportlab-2.5-py2.7-linux-x86_64.egg/reportlab/platypus/doctemplate.py", line 793, in handle_flowable
    raise LayoutError(ident)
reportlab.platypus.doctemplate.LayoutError: Flowable <Spacer at 0xf01440 frame=normal>...(1 x 14.4) too large on page 10 in frame 'normal'(439.275590551 x 628.28976378) of template 'Later'