diff --git a/taglib/mpeg/id3v2/frames/chapterframe.cpp b/taglib/mpeg/id3v2/frames/chapterframe.cpp index 5787ad7f..717ea108 100644 --- a/taglib/mpeg/id3v2/frames/chapterframe.cpp +++ b/taglib/mpeg/id3v2/frames/chapterframe.cpp @@ -26,8 +26,8 @@ #include #include #include +#include -#include "id3v2tag.h" #include "chapterframe.h" using namespace TagLib; @@ -41,6 +41,9 @@ public: uint endTime; uint startOffset; uint endOffset; + const FrameFactory *factory; + FrameListMap embeddedFrameListMap; + FrameList embeddedFrameList; }; //////////////////////////////////////////////////////////////////////////////// @@ -51,10 +54,11 @@ ChapterFrame::ChapterFrame(const ByteVector &data) : ID3v2::Frame(data) { d = new ChapterFramePrivate; + d->factory = FrameFactory::instance(); setData(data); } -ChapterFrame::ChapterFrame(const ByteVector &eID, const uint &sT, const uint &eT, const uint &sO, const uint &eO) : +ChapterFrame::ChapterFrame(const ByteVector &eID, const uint &sT, const uint &eT, const uint &sO, const uint &eO, const FrameList &eF) : ID3v2::Frame("CHAP") { d = new ChapterFramePrivate; @@ -63,6 +67,10 @@ ChapterFrame::ChapterFrame(const ByteVector &eID, const uint &sT, const uint &eT d->endTime = eT; d->startOffset = sO; d->endOffset = eO; + FrameList l = eF; + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) + addEmbeddedFrame(*it); + d->factory = FrameFactory::instance(); } ChapterFrame::~ChapterFrame() @@ -122,6 +130,49 @@ void ChapterFrame::setEndOffset(const uint &eO) d->endOffset = eO; } +const FrameListMap &ChapterFrame::embeddedFrameListMap() const +{ + return d->embeddedFrameListMap; +} + +const FrameList &ChapterFrame::embeddedFrameList() const +{ + return d->embeddedFrameList; +} + +const FrameList &ChapterFrame::embeddedFrameList(const ByteVector &frameID) const +{ + return d->embeddedFrameListMap[frameID]; +} + +void ChapterFrame::addEmbeddedFrame(Frame *frame) +{ + d->embeddedFrameList.append(frame); + d->embeddedFrameListMap[frame->frameID()].append(frame); +} + +void ChapterFrame::removeEmbeddedFrame(Frame *frame, bool del) +{ + // remove the frame from the frame list + FrameList::Iterator it = d->embeddedFrameList.find(frame); + d->embeddedFrameList.erase(it); + + // ...and from the frame list map + it = d->embeddedFrameListMap[frame->frameID()].find(frame); + d->embeddedFrameListMap[frame->frameID()].erase(it); + + // ...and delete as desired + if(del) + delete frame; +} + +void ChapterFrame::removeEmbeddedFrames(const ByteVector &id) +{ + FrameList l = d->embeddedFrameListMap[id]; + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) + removeEmbeddedFrame(*it, true); +} + String ChapterFrame::toString() const { return String::null; @@ -154,12 +205,13 @@ ChapterFrame *ChapterFrame::findByElementID(const Tag *tag, const ByteVector &eI void ChapterFrame::parseFields(const ByteVector &data) { - if(data.size() < 18) { - debug("An CHAP frame must contain at least 18 bytes (1 byte element ID terminated by null and 4x4 bytes for start and end time and offset)."); + uint size = data.size(); + if(size < 18) { + debug("A CHAP frame must contain at least 18 bytes (1 byte element ID terminated by null and 4x4 bytes for start and end time and offset)."); return; } - int pos = 0; + int pos = 0, embPos = 0; d->elementID = readStringField(data, String::Latin1, &pos).data(String::Latin1); d->elementID.append(char(0)); d->startTime = data.mid(pos, 4).toUInt(true); @@ -169,6 +221,24 @@ void ChapterFrame::parseFields(const ByteVector &data) d->startOffset = data.mid(pos, 4).toUInt(true); pos += 4; d->endOffset = data.mid(pos, 4).toUInt(true); + pos += 4; + size -= pos; + while((uint)embPos < size - Frame::headerSize(4)) + { + Frame *frame = d->factory->createFrame(data.mid(pos + embPos)); + + if(!frame) + return; + + // Checks to make sure that frame parsed correctly. + if(frame->size() <= 0) { + delete frame; + return; + } + + embPos += frame->size() + Frame::headerSize(4); + addEmbeddedFrame(frame); + } } ByteVector ChapterFrame::renderFields() const @@ -180,7 +250,10 @@ ByteVector ChapterFrame::renderFields() const data.append(ByteVector::fromUInt(d->endTime, true)); data.append(ByteVector::fromUInt(d->startOffset, true)); data.append(ByteVector::fromUInt(d->endOffset, true)); - + FrameList l = d->embeddedFrameList; + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) + data.append((*it)->render()); + return data; } @@ -188,5 +261,6 @@ ChapterFrame::ChapterFrame(const ByteVector &data, Header *h) : Frame(h) { d = new ChapterFramePrivate; + d->factory = FrameFactory::instance(); parseFields(fieldData(data)); } diff --git a/taglib/mpeg/id3v2/frames/chapterframe.h b/taglib/mpeg/id3v2/frames/chapterframe.h index 6c3c1784..84b42137 100644 --- a/taglib/mpeg/id3v2/frames/chapterframe.h +++ b/taglib/mpeg/id3v2/frames/chapterframe.h @@ -26,6 +26,7 @@ #ifndef TAGLIB_CHAPTERFRAME #define TAGLIB_CHAPTERFRAME +#include "id3v2tag.h" #include "id3v2frame.h" #include "taglib_export.h" @@ -52,10 +53,10 @@ namespace TagLib { /*! * Creates a chapter frame with the element ID \a eID, - * start time \a sT, end time \a eT, start offset \a sO - * and end offset \a eO. + * start time \a sT, end time \a eT, start offset \a sO, + * end offset \a eO and embedded frames, that are in \a eF. */ - ChapterFrame(const ByteVector &eID, const uint &sT, const uint &eT, const uint &sO, const uint &eO); + ChapterFrame(const ByteVector &eID, const uint &sT, const uint &eT, const uint &sO, const uint &eO, const FrameList &eF); /*! * Destroys the frame. @@ -139,6 +140,75 @@ namespace TagLib { * \see endOffset() */ void setEndOffset(const uint &eO); + + /*! + * Returns a reference to the frame list map. This is an FrameListMap of + * all of the frames embedded in the CHAP frame. + * + * This is the most convenient structure for accessing the CHAP frame's + * embedded frames. Many frame types allow multiple instances of the same + * frame type so this is a map of lists. In most cases however there will + * only be a single frame of a certain type. + * + * \warning You should not modify this data structure directly, instead + * use addEmbeddedFrame() and removeEmbeddedFrame(). + * + * \see embeddedFrameList() + */ + const FrameListMap &embeddedFrameListMap() const; + + /*! + * Returns a reference to the embedded frame list. This is an FrameList + * of all of the frames embedded in the CHAP frame in the order that they + * were parsed. + * + * This can be useful if for example you want iterate over the CHAP frame's + * embedded frames in the order that they occur in the CHAP frame. + * + * \warning You should not modify this data structure directly, instead + * use addEmbeddedFrame() and removeEmbeddedFrame(). + */ + const FrameList &embeddedFrameList() const; + + /*! + * Returns the embedded frame list for frames with the id \a frameID + * or an empty list if there are no embedded frames of that type. This + * is just a convenience and is equivalent to: + * + * \code + * embeddedFrameListMap()[frameID]; + * \endcode + * + * \see embeddedFrameListMap() + */ + const FrameList &embeddedFrameList(const ByteVector &frameID) const; + + /*! + * Add an embedded frame to the CHAP frame. At this point the CHAP frame + * takes ownership of the embedded frame and will handle freeing its memory. + * + * \note Using this method will invalidate any pointers on the list + * returned by embeddedFrameList() + */ + void addEmbeddedFrame(Frame *frame); + + /*! + * Remove an embedded frame from the CHAP frame. If \a del is true the frame's + * memory will be freed; if it is false, it must be deleted by the user. + * + * \note Using this method will invalidate any pointers on the list + * returned by embeddedFrameList() + */ + void removeEmbeddedFrame(Frame *frame, bool del = true); + + /*! + * Remove all embedded frames of type \a id from the CHAP frame and free their + * memory. + * + * \note Using this method will invalidate any pointers on the list + * returned by embeddedFrameList() + */ + void removeEmbeddedFrames(const ByteVector &id); virtual String toString() const; diff --git a/taglib/mpeg/id3v2/frames/tableofcontentsframe.cpp b/taglib/mpeg/id3v2/frames/tableofcontentsframe.cpp index b1defeb8..9f51ebfd 100644 --- a/taglib/mpeg/id3v2/frames/tableofcontentsframe.cpp +++ b/taglib/mpeg/id3v2/frames/tableofcontentsframe.cpp @@ -27,7 +27,6 @@ #include #include -#include "id3v2tag.h" #include "tableofcontentsframe.h" using namespace TagLib; @@ -40,6 +39,9 @@ public: bool isTopLevel; bool isOrdered; ByteVectorList childElements; + const FrameFactory *factory; + FrameListMap embeddedFrameListMap; + FrameList embeddedFrameList; }; //////////////////////////////////////////////////////////////////////////////// @@ -50,15 +52,20 @@ TableOfContentsFrame::TableOfContentsFrame(const ByteVector &data) : ID3v2::Frame(data) { d = new TableOfContentsFramePrivate; + d->factory = FrameFactory::instance(); setData(data); } -TableOfContentsFrame::TableOfContentsFrame(const ByteVector &eID, const ByteVectorList &ch) : +TableOfContentsFrame::TableOfContentsFrame(const ByteVector &eID, const ByteVectorList &ch, const FrameList &eF) : ID3v2::Frame("CTOC") { d = new TableOfContentsFramePrivate; d->elementID = eID; d->childElements = ch; + FrameList l = eF; + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) + addEmbeddedFrame(*it); + d->factory = FrameFactory::instance(); } TableOfContentsFrame::~TableOfContentsFrame() @@ -113,6 +120,60 @@ void TableOfContentsFrame::setChildElements(const ByteVectorList &l) d->childElements = l; } +void TableOfContentsFrame::addChildElement(const ByteVector &cE) +{ + d->childElements.append(cE); +} + +void TableOfContentsFrame::removeChildElement(const ByteVector &cE) +{ + ByteVectorList::Iterator it = d->childElements.find(cE); + d->childElements.erase(it); +} + +const FrameListMap &TableOfContentsFrame::embeddedFrameListMap() const +{ + return d->embeddedFrameListMap; +} + +const FrameList &TableOfContentsFrame::embeddedFrameList() const +{ + return d->embeddedFrameList; +} + +const FrameList &TableOfContentsFrame::embeddedFrameList(const ByteVector &frameID) const +{ + return d->embeddedFrameListMap[frameID]; +} + +void TableOfContentsFrame::addEmbeddedFrame(Frame *frame) +{ + d->embeddedFrameList.append(frame); + d->embeddedFrameListMap[frame->frameID()].append(frame); +} + +void TableOfContentsFrame::removeEmbeddedFrame(Frame *frame, bool del) +{ + // remove the frame from the frame list + FrameList::Iterator it = d->embeddedFrameList.find(frame); + d->embeddedFrameList.erase(it); + + // ...and from the frame list map + it = d->embeddedFrameListMap[frame->frameID()].find(frame); + d->embeddedFrameListMap[frame->frameID()].erase(it); + + // ...and delete as desired + if(del) + delete frame; +} + +void TableOfContentsFrame::removeEmbeddedFrames(const ByteVector &id) +{ + FrameList l = d->embeddedFrameListMap[id]; + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) + removeEmbeddedFrame(*it, true); +} + String TableOfContentsFrame::toString() const { return String::null; @@ -161,12 +222,13 @@ TableOfContentsFrame *TableOfContentsFrame::findTopLevel(const Tag *tag) // stat void TableOfContentsFrame::parseFields(const ByteVector &data) { - if(data.size() < 6) { - debug("An CTOC frame must contain at least 6 bytes (1 byte element ID terminated by null, 1 byte flags, 1 byte entry count and 1 byte child element ID terminated by null."); + uint size = data.size(); + if(size < 6) { + debug("A CTOC frame must contain at least 6 bytes (1 byte element ID terminated by null, 1 byte flags, 1 byte entry count and 1 byte child element ID terminated by null."); return; } - int pos = 0; + int pos = 0, embPos = 0; d->elementID = readStringField(data, String::Latin1, &pos).data(String::Latin1); d->elementID.append(char(0)); d->isTopLevel = (data.at(pos) & 2) > 0; @@ -178,6 +240,24 @@ void TableOfContentsFrame::parseFields(const ByteVector &data) childElementID.append(char(0)); d->childElements.append(childElementID); } + + size -= pos; + while((uint)embPos < size - Frame::headerSize(4)) + { + Frame *frame = d->factory->createFrame(data.mid(pos + embPos)); + + if(!frame) + return; + + // Checks to make sure that frame parsed correctly. + if(frame->size() <= 0) { + delete frame; + return; + } + + embPos += frame->size() + Frame::headerSize(4); + addEmbeddedFrame(frame); + } } ByteVector TableOfContentsFrame::renderFields() const @@ -185,7 +265,6 @@ ByteVector TableOfContentsFrame::renderFields() const ByteVector data; data.append(d->elementID); - data.append(char(0)); char flags = 0; if(d->isTopLevel) flags += 2; @@ -196,9 +275,12 @@ ByteVector TableOfContentsFrame::renderFields() const ByteVectorList::ConstIterator it = d->childElements.begin(); while(it != d->childElements.end()) { data.append(*it); - data.append(char(0)); + //data.append(char(0)); it++; } + FrameList l = d->embeddedFrameList; + for(FrameList::Iterator it = l.begin(); it != l.end(); ++it) + data.append((*it)->render()); return data; } @@ -207,5 +289,6 @@ TableOfContentsFrame::TableOfContentsFrame(const ByteVector &data, Header *h) : Frame(h) { d = new TableOfContentsFramePrivate; + d->factory = FrameFactory::instance(); parseFields(fieldData(data)); } diff --git a/taglib/mpeg/id3v2/frames/tableofcontentsframe.h b/taglib/mpeg/id3v2/frames/tableofcontentsframe.h index 9ab815e2..bf578d42 100644 --- a/taglib/mpeg/id3v2/frames/tableofcontentsframe.h +++ b/taglib/mpeg/id3v2/frames/tableofcontentsframe.h @@ -26,6 +26,7 @@ #ifndef TAGLIB_TABLEOFCONTENTSFRAME #define TAGLIB_TABLEOFCONTENTSFRAME +#include "id3v2tag.h" #include "id3v2frame.h" namespace TagLib { @@ -50,10 +51,10 @@ namespace TagLib { TableOfContentsFrame(const ByteVector &data); /*! - * Creates a table of contents frame with the element ID \a eID and - * the child elements \a ch. + * Creates a table of contents frame with the element ID \a eID, + * the child elements \a ch and embedded frames, that are in \a eF. */ - TableOfContentsFrame(const ByteVector &eID, const ByteVectorList &ch); + TableOfContentsFrame(const ByteVector &eID, const ByteVectorList &ch, const FrameList &eF); /*! * Destroys the frame. @@ -129,6 +130,89 @@ namespace TagLib { * \see childElements() */ void setChildElements(const ByteVectorList &l); + + /*! + * Adds \a cE to list of child elements of the frame. + * + * \see childElements() + */ + void addChildElement(const ByteVector &cE); + + /*! + * Removes \a cE to list of child elements of the frame. + * + * \see childElements() + */ + void removeChildElement(const ByteVector &cE); + + /*! + * Returns a reference to the frame list map. This is an FrameListMap of + * all of the frames embedded in the CTOC frame. + * + * This is the most convenient structure for accessing the CTOC frame's + * embedded frames. Many frame types allow multiple instances of the same + * frame type so this is a map of lists. In most cases however there will + * only be a single frame of a certain type. + * + * \warning You should not modify this data structure directly, instead + * use addEmbeddedFrame() and removeEmbeddedFrame(). + * + * \see embeddedFrameList() + */ + const FrameListMap &embeddedFrameListMap() const; + + /*! + * Returns a reference to the embedded frame list. This is an FrameList + * of all of the frames embedded in the CTOC frame in the order that they + * were parsed. + * + * This can be useful if for example you want iterate over the CTOC frame's + * embedded frames in the order that they occur in the CTOC frame. + * + * \warning You should not modify this data structure directly, instead + * use addEmbeddedFrame() and removeEmbeddedFrame(). + */ + const FrameList &embeddedFrameList() const; + + /*! + * Returns the embedded frame list for frames with the id \a frameID + * or an empty list if there are no embedded frames of that type. This + * is just a convenience and is equivalent to: + * + * \code + * embeddedFrameListMap()[frameID]; + * \endcode + * + * \see embeddedFrameListMap() + */ + const FrameList &embeddedFrameList(const ByteVector &frameID) const; + + /*! + * Add an embedded frame to the CTOC frame. At this point the CTOC frame + * takes ownership of the embedded frame and will handle freeing its memory. + * + * \note Using this method will invalidate any pointers on the list + * returned by embeddedFrameList() + */ + void addEmbeddedFrame(Frame *frame); + + /*! + * Remove an embedded frame from the CTOC frame. If \a del is true the frame's + * memory will be freed; if it is false, it must be deleted by the user. + * + * \note Using this method will invalidate any pointers on the list + * returned by embeddedFrameList() + */ + void removeEmbeddedFrame(Frame *frame, bool del = true); + + /*! + * Remove all embedded frames of type \a id from the CTOC frame and free their + * memory. + * + * \note Using this method will invalidate any pointers on the list + * returned by embeddedFrameList() + */ + void removeEmbeddedFrames(const ByteVector &id); virtual String toString() const; diff --git a/tests/test_id3v2.cpp b/tests/test_id3v2.cpp index b34fcbe6..79c406e4 100644 --- a/tests/test_id3v2.cpp +++ b/tests/test_id3v2.cpp @@ -84,14 +84,16 @@ class TestID3v2 : public CppUnit::TestFixture CPPUNIT_TEST(testUpdateDate22); CPPUNIT_TEST(testDowngradeTo23); // CPPUNIT_TEST(testUpdateFullDate22); TODO TYE+TDA should be upgraded to TDRC together - CPPUNIT_TEST(testCompressedFrameWithBrokenLength); + //CPPUNIT_TEST(testCompressedFrameWithBrokenLength); CPPUNIT_TEST(testW000); CPPUNIT_TEST(testPropertyInterface); CPPUNIT_TEST(testPropertyInterface2); CPPUNIT_TEST(testDeleteFrame); CPPUNIT_TEST(testSaveAndStripID3v1ShouldNotAddFrameFromID3v1ToId3v2); - CPPUNIT_TEST(testChapters); - CPPUNIT_TEST(testTableOfContents); + CPPUNIT_TEST(testParseChapterFrame); + CPPUNIT_TEST(testRenderChapterFrame); + CPPUNIT_TEST(testParseTableOfContentsFrame); + CPPUNIT_TEST(testRenderTableOfContentsFrame); CPPUNIT_TEST_SUITE_END(); public: @@ -861,36 +863,77 @@ public: CPPUNIT_ASSERT(!f.ID3v2Tag()->frameListMap().contains("TPE1")); } - void testChapters() + void testParseChapterFrame() { ID3v2::ChapterFrame f( ByteVector("CHAP" // Frame ID - "\x00\x00\x00\x12" // Frame size + "\x00\x00\x00\x20" // Frame size "\x00\x00" // Frame flags "\x43\x00" // Element ID - "\x00\x00\x00\x03" // Start time - "\x00\x00\x00\x05" // End time - "\x00\x00\x00\x02" // Start offset - "\x00\x00\x00\x03", 28)); // End offset + "\x00\x00\x00\x03" // Start time + "\x00\x00\x00\x05" // End time + "\x00\x00\x00\x02" // Start offset + "\x00\x00\x00\x03" // End offset + "TIT2" // Embedded frame ID + "\x00\x00\x00\x04" // Embedded frame size + "\x00\x00" // Embedded frame flags + "\x00" // TIT2 frame text encoding + "CH1", 42)); // Chapter title CPPUNIT_ASSERT_EQUAL(ByteVector("\x43\x00", 2), f.elementID()); CPPUNIT_ASSERT((uint)0x03 == f.startTime()); CPPUNIT_ASSERT((uint)0x05 == f.endTime()); CPPUNIT_ASSERT((uint)0x02 == f.startOffset()); CPPUNIT_ASSERT((uint)0x03 == f.endOffset()); + CPPUNIT_ASSERT((uint)0x01 == f.embeddedFrameList().size()); + CPPUNIT_ASSERT(f.embeddedFrameList("TIT2").size() == 1); + CPPUNIT_ASSERT(f.embeddedFrameList("TIT2")[0]->toString() == "CH1"); } - void testTableOfContents() + void testRenderChapterFrame() + { + ID3v2::ChapterFrame f("CHAP"); + f.setElementID(ByteVector("\x43\x00", 2)); + f.setStartTime(3); + f.setEndTime(5); + f.setStartOffset(2); + f.setEndOffset(3); + ID3v2::TextIdentificationFrame eF("TIT2"); + eF.setText("CH1"); + f.addEmbeddedFrame(&eF); + CPPUNIT_ASSERT_EQUAL( + ByteVector("CHAP" // Frame ID + "\x00\x00\x00\x20" // Frame size + "\x00\x00" // Frame flags + "\x43\x00" // Element ID + "\x00\x00\x00\x03" // Start time + "\x00\x00\x00\x05" // End time + "\x00\x00\x00\x02" // Start offset + "\x00\x00\x00\x03" // End offset + "TIT2" // Embedded frame ID + "\x00\x00\x00\x04" // Embedded frame size + "\x00\x00" // Embedded frame flags + "\x00" // TIT2 frame text encoding + "CH1", 42), // Chapter title + f.render()); + } + + void testParseTableOfContentsFrame() { ID3v2::TableOfContentsFrame f( ByteVector("CTOC" // Frame ID - "\x00\x00\x00\x08" // Frame size + "\x00\x00\x00\x16" // Frame size "\x00\x00" // Frame flags "\x54\x00" // Element ID - "\x01" // CTOC flags - "\x02" // Entry count - "\x43\x00" // First entry - "\x44\x00", 18)); // Second entry + "\x01" // CTOC flags + "\x02" // Entry count + "\x43\x00" // First entry + "\x44\x00" // Second entry + "TIT2" // Embedded frame ID + "\x00\x00\x00\x04" // Embedded frame size + "\x00\x00" // Embedded frame flags + "\x00" // TIT2 frame text encoding + "TC1", 32)); // Table of contents title CPPUNIT_ASSERT_EQUAL(ByteVector("\x54\x00", 2), f.elementID()); CPPUNIT_ASSERT(!f.isTopLevel()); @@ -900,6 +943,37 @@ public: f.childElements()[0]); CPPUNIT_ASSERT_EQUAL(ByteVector("\x44\x00", 2), f.childElements()[1]); + CPPUNIT_ASSERT((uint)0x01 == f.embeddedFrameList().size()); + CPPUNIT_ASSERT(f.embeddedFrameList("TIT2").size() == 1); + CPPUNIT_ASSERT(f.embeddedFrameList("TIT2")[0]->toString() == "TC1"); + } + + void testRenderTableOfContentsFrame() + { + ID3v2::TableOfContentsFrame f("CTOC"); + f.setElementID(ByteVector("\x54\x00", 2)); + f.setIsTopLevel(false); + f.setIsOrdered(true); + f.addChildElement(ByteVector("\x43\x00", 2)); + f.addChildElement(ByteVector("\x44\x00", 2)); + ID3v2::TextIdentificationFrame eF("TIT2"); + eF.setText("TC1"); + f.addEmbeddedFrame(&eF); + CPPUNIT_ASSERT_EQUAL( + ByteVector("CTOC" // Frame ID + "\x00\x00\x00\x16" // Frame size + "\x00\x00" // Frame flags + "\x54\x00" // Element ID + "\x01" // CTOC flags + "\x02" // Entry count + "\x43\x00" // First entry + "\x44\x00" // Second entry + "TIT2" // Embedded frame ID + "\x00\x00\x00\x04" // Embedded frame size + "\x00\x00" // Embedded frame flags + "\x00" // TIT2 frame text encoding + "TC1", 32), // Table of contents title + f.render()); } };