/* * _CPRTFProducer.j * * Serialize CPAttributedString to a RTF String * * Copyright (C) 2014 Daniel Boehringer * This file is based on the RTFProducer from GNUStep * (which I co-authored with Fred Kiefer in 1999) * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ @import @import "CPParagraphStyle.j" @import "CPColor.j" @import "CPGraphics.j" @import "CPFontManager.j" @import "_CPTableTextAttachment.j" @global CPForegroundColorAttributeName @global CPBackgroundColorAttributeName @global CPUnderlineStyleAttributeName @global CPSuperscriptAttributeName @global CPBaselineOffsetAttributeName @global CPAttachmentAttributeName @global CPLigatureAttributeName @global CPKernAttributeName @global CPLeftTextAlignment @global CPRightTextAlignment @global CPCenterTextAlignment @global CPJustifiedTextAlignment @global CPNaturalTextAlignment @global CPLeftTabStopType @global CPRightTabStopType @global CPCenterTabStopType @global CPDecimalTabStopType var PAPERSIZE = @"PaperSize", LEFTMARGIN = @"LeftMargin", RIGHTMARGIN = @"RightMargin", TOPMARGIN = @"TopMargin", BUTTOMMARGIN = @"ButtomMargin"; function _points2twips(a) { return (a) * 20.0; } @implementation _CPRTFProducer : CPObject { CPAttributedString text; CPMutableDictionary fontDict; CPMutableDictionary colorDict; CPDictionary docDict; CPMutableArray attachments; CPFont currentFont; CPColor fgColor; CPColor bgColor; CPColor ulColor; } // MARK: - // MARK: Class methods + (CPString)produceRTF:(CPAttributedString)aText documentAttributes:(CPDictionary)dict { var mynew = [self new]; return [mynew RTFDStringFromAttributedString:aText documentAttributes:dict]; } // MARK: - // MARK: init methods - (id)init { if (self = [super init]) { colorDict = [CPMutableDictionary new]; fontDict = [CPMutableDictionary new]; fgColor = [CPColor blackColor]; bgColor= [CPColor whiteColor]; } return self; } - (CPString)fontTable { if (![fontDict count]) return @""; var fontlistString = "", fontEnum, currFont, keyArray; keyArray = [fontDict allKeys]; keyArray = [keyArray sortedArrayUsingSelector:@selector(compare:)]; fontEnum = [keyArray objectEnumerator]; while ((currFont = [fontEnum nextObject]) != nil) { var fontFamily, detail; if ([currFont isEqualToString:@"Symbol"]) fontFamily = @"tech"; else if ([currFont isEqualToString:@"Helvetica"]) fontFamily = @"swiss"; else if ([currFont isEqualToString:@"Arial"]) fontFamily = @"swiss"; else if ([currFont isEqualToString:@"Courier"]) fontFamily = @"modern"; else if ([currFont isEqualToString:@"Times"]) fontFamily = @"roman"; else fontFamily = @"nil"; detail = [CPString stringWithFormat:@"%@\\f%@ %@;", [fontDict objectForKey:currFont], fontFamily, currFont]; fontlistString += detail; } return [CPString stringWithFormat:@"{\\fonttbl%@}\n", fontlistString]; } - (CPString)colorTable { if (![colorDict count]) return @""; var result, count = [colorDict count], list = [CPMutableArray arrayWithCapacity:count], keyEnum = [colorDict keyEnumerator], next, i; while ((next = [keyEnum nextObject]) != nil) { var cn = [colorDict objectForKey:next]; [list insertObject:[CPColor colorWithCSSString:next] atIndex:[cn intValue]-1]; } result = [CPString stringWithString:@"{\\colortbl;"]; for (i = 0; i < count; i++) { var color = [[list objectAtIndex:i] colorUsingColorSpaceName:CPCalibratedRGBColorSpace]; result += [CPString stringWithFormat:@"\\red%d\\green%d\\blue%d;", ([color redComponent] * 255), ([color greenComponent] * 255), ([color blueComponent] * 255)]; } result += @"}\n"; return result; } - (CPString)documentAttributes { if (!docDict) return @""; var result, detail, val, num; result = [CPString string]; val = [docDict objectForKey:PAPERSIZE]; if (val) { detail = [CPString stringWithFormat:@"\\paperw%d \\paperh%d", _points2twips(val.width), _points2twips(val.height)]; result += detail; } num = [docDict objectForKey:LEFTMARGIN]; if (num) { var f = [num floatValue]; detail = [CPString stringWithFormat:@"\\margl%d", _points2twips(f)]; result += detail; } num = [docDict objectForKey:RIGHTMARGIN]; if (num) { var f = [num floatValue]; detail = [CPString stringWithFormat:@"\\margr%d", _points2twips(f)]; result += detail; } num = [docDict objectForKey:TOPMARGIN]; if (num) { var f = [num floatValue]; detail = [CPString stringWithFormat:@"\\margt%d", _points2twips(f)]; result += detail; } num = [docDict objectForKey:BUTTOMMARGIN]; if (num) { var f = [num floatValue]; detail = [CPString stringWithFormat:@"\\margb%d", _points2twips(f)]; result += detail; } return result; } - (CPString)headerString { var result; result = [CPString stringWithString:@"{\\rtf1\\ansi"]; result += [self fontTable]; result += [self colorTable]; result += [self documentAttributes]; return result; } - (CPString)trailerString { return @"}"; } - (CPString)fontToken:(CPString) fontName { var fCount = [fontDict objectForKey:fontName]; if (fCount == nil) { var count = [fontDict count]; fCount = [CPString stringWithFormat:@"\\f%d", count]; [fontDict setObject:fCount forKey:fontName]; } return fCount; } - (int)numberForColor:(CPColor)color { var num = [colorDict objectForKey:[color cssString]]; if (!num) [colorDict setObject:num = [CPNumber numberWithInt:[colorDict count] + 1] forKey:[color cssString]]; return [num intValue]; } - (CPString)paragraphStyle:(CPParagraphStyle)paraStyle { var headerString = [CPString stringWithString:@"\\pard"], twips; if (paraStyle == nil) return headerString; switch ([paraStyle alignment]) { case CPRightTextAlignment: headerString += @"\\qr"; break; case CPCenterTextAlignment: headerString += @"\\qc"; break; case CPLeftTextAlignment: headerString += @"\\ql"; break; case CPJustifiedTextAlignment: headerString += @"\\qj"; break; default: headerString += @"\\ql"; break; } var twips = _points2twips([paraStyle firstLineHeadIndent]); if (twips != 0.0) headerString += [CPString stringWithFormat:@"\\fi%d", twips]; twips = _points2twips([paraStyle headIndent]); if (twips != 0.0) headerString += [CPString stringWithFormat:@"\\li%d", twips]; twips = _points2twips([paraStyle tailIndent]); if (twips != 0.0) headerString += [CPString stringWithFormat:@"\\ri%d", twips]; twips = _points2twips([paraStyle paragraphSpacing]); if (twips != 0.0) headerString += [CPString stringWithFormat:@"\\sa%d", twips]; twips = _points2twips([paraStyle minimumLineHeight]); if (twips != 0.0) headerString += [CPString stringWithFormat:@"\\sl%d", twips]; twips = _points2twips([paraStyle maximumLineHeight]); if (twips != 0.0) headerString += [CPString stringWithFormat:@"\\sl-%d", twips]; var enumerator, tab; enumerator = [[paraStyle tabStops] objectEnumerator]; while ((tab = [enumerator nextObject])) { var tabType = [tab respondsToSelector:@selector(tabStopType)] ? [tab tabStopType] : nil; if (tabType === nil && [tab respondsToSelector:@selector(alignment)]) tabType = [tab alignment]; switch (tabType) { case CPLeftTabStopType: case CPLeftTextAlignment: break; case CPRightTabStopType: case CPRightTextAlignment: headerString += @"\\tqr"; break; case CPCenterTabStopType: case CPCenterTextAlignment: headerString += @"\\tqc"; break; case CPDecimalTabStopType: headerString += @"\\tqdec"; break; } headerString += [CPString stringWithFormat:@"\\tx%d",_points2twips([tab location])]; } return headerString; } - (CPString)runStringForString:(CPString) substring attributes:(CPDictionary) attributes paragraphStart:(BOOL) first { var unwrap = function(obj) { if (!obj) return null; if ((typeof obj.respondsToSelector === "function" && ([obj respondsToSelector:@selector(headers)] || [obj respondsToSelector:@selector(rows)])) || obj.headers || obj._headers || obj.rows || obj._rows) { return obj; } var unwrapped = null; if (typeof obj.respondsToSelector === "function") { if ([obj respondsToSelector:@selector(attachmentCell)]) { unwrapped = [obj attachmentCell]; } else if ([obj respondsToSelector:@selector(content)]) { unwrapped = [obj content]; } else if ([obj respondsToSelector:@selector(view)]) { unwrapped = [obj view]; } } if (!unwrapped) { unwrapped = obj._attachmentCell || obj._content || obj._view || obj.attachmentCell || obj.content || obj.view; } return unwrapped ? unwrapped : obj; }; var tableAttachment = null; if (typeof CPAttachmentAttributeName !== "undefined") { tableAttachment = [attributes objectForKey:CPAttachmentAttributeName]; } if (!tableAttachment) { tableAttachment = [attributes objectForKey:@"CPAttachmentAttributeName"]; } if (!tableAttachment) { tableAttachment = [attributes objectForKey:@"TableAttachmentAttribute"]; } if (!tableAttachment) { tableAttachment = [attributes objectForKey:@"_CPAttachmentView"]; } if (!tableAttachment && typeof _CPAttachmentView !== "undefined") { tableAttachment = [attributes objectForKey:_CPAttachmentView]; } tableAttachment = unwrap(tableAttachment); if (!tableAttachment && (substring === "\uFFFC" || substring === "")) { var keys = [attributes allKeys], count = [keys count]; for (var i = 0; i < count; i++) { var key = [keys objectAtIndex:i], val = unwrap([attributes objectForKey:key]); if (val && ( (typeof val.respondsToSelector === "function" && [val respondsToSelector:@selector(headers)]) || val._headers || val.headers || (typeof _CPTableTextAttachment !== "undefined" && [val isKindOfClass:[_CPTableTextAttachment class]]) )) { tableAttachment = val; break; } } } if (tableAttachment) { var headers = null, rows = null; // Try to fetch from the active live view of the attachment first to capture user edits var activeView = null; if (typeof tableAttachment.respondsToSelector === "function" && [tableAttachment respondsToSelector:@selector(view)]) { activeView = [tableAttachment view]; } if (!activeView) { activeView = tableAttachment._view || tableAttachment.view; } if (activeView) { if (typeof activeView.respondsToSelector === "function") { if ([activeView respondsToSelector:@selector(headers)]) { headers = [activeView headers]; } if ([activeView respondsToSelector:@selector(rows)]) { rows = [activeView rows]; } } if (!headers) { headers = activeView._headers || activeView.headers; } if (!rows) { rows = activeView._rows || activeView.rows; } } // Fall back to the attachment's parsed properties if the view is nil or lacks the properties if (!headers || !rows) { if (typeof tableAttachment.respondsToSelector === "function") { if ([tableAttachment respondsToSelector:@selector(headers)]) { headers = [tableAttachment headers]; } if ([tableAttachment respondsToSelector:@selector(rows)]) { rows = [tableAttachment rows]; } } if (!headers) { headers = tableAttachment._headers || tableAttachment.headers; } if (!rows) { rows = tableAttachment._rows || tableAttachment.rows; } } var getCount = function(arr) { if (!arr) return 0; if (typeof arr.count === "function") return [arr count]; return arr.length; }; var getObjectAtIndex = function(arr, idx) { if (!arr) return null; if (typeof arr.objectAtIndex === "function") return [arr objectAtIndex:idx]; return arr[idx]; }; var numCols = getCount(headers); if (numCols == 0 && getCount(rows) > 0) { numCols = getCount(getObjectAtIndex(rows, 0)); } if (numCols > 0) { var totalWidthTwips = 10000; var colWidthTwips = Math.floor(totalWidthTwips / numCols); var cellBoundaries = []; var currentBoundary = 0; for (var i = 0; i < numCols; i++) { currentBoundary += colWidthTwips; cellBoundaries.push(currentBoundary); } var tableRTF = ""; var writeRow = function(rowData, isHeaderRow) { var rowRTF = "\\trowd\\trgaph115\\trleft0"; for (var c = 0; c < numCols; c++) { rowRTF += "\\clbrdrt\\brdrs\\brdrw10\\clbrdrb\\brdrs\\brdrw10\\clbrdrl\\brdrs\\brdrw10\\clbrdrr\\brdrs\\brdrw10"; rowRTF += "\\cellx" + cellBoundaries[c]; } for (var c = 0; c < numCols; c++) { var cellText = ""; if (c < getCount(rowData)) { cellText = getObjectAtIndex(rowData, c); } if (cellText === null || cellText === undefined) { cellText = ""; } // Safely extract text representation from CPAttributedString / CPTextStorage if present if (cellText && typeof cellText === "object") { if (typeof cellText.string === "function") { cellText = [cellText string]; } else if (cellText._string !== undefined) { cellText = cellText._string; } else if (cellText.string !== undefined) { cellText = cellText.string; } } cellText = String(cellText); cellText = cellText.replace(/\\/g, '\\\\'); cellText = cellText.replace(/{/g, '\\{'); cellText = cellText.replace(/}/g, '\\}'); cellText = cellText.replace(/\n/g, '\\line '); if (isHeaderRow) { rowRTF += "{\\intbl\\b " + cellText + "\\b0\\cell}"; } else { rowRTF += "{\\intbl " + cellText + "\\cell}"; } } rowRTF += "\\row\n"; return rowRTF; }; if (getCount(headers) > 0) { tableRTF += writeRow(headers, YES); } var rowCount = getCount(rows); for (var r = 0; r < rowCount; r++) { tableRTF += writeRow(getObjectAtIndex(rows, r), NO); } return tableRTF; } } var result = "", headerString = "", trailerString = "", attribEnum, currAttrib; if (first) { var paraStyle = [attributes objectForKey:CPParagraphStyleAttributeName]; headerString += [self paragraphStyle:paraStyle]; } attribEnum = [attributes keyEnumerator]; while ((currAttrib = [attribEnum nextObject]) != nil) { if ([currAttrib isEqualToString:CPFontAttributeName]) { var font, fontName, traits; font = [attributes objectForKey:CPFontAttributeName]; fontName = [font familyName]; traits = [[CPFontManager sharedFontManager] traitsOfFont:font]; if (currentFont == nil || ![fontName isEqualToString:[currentFont familyName]]) headerString += [self fontToken:fontName]; if (currentFont == nil || [font size] != [currentFont size]) { var points = [font size] * 2, pString; pString = [CPString stringWithFormat:@"\\fs%d", points]; headerString += pString; } if (traits & CPItalicFontMask) { headerString += @"\\i"; trailerString += @"\\i0"; } if (traits & CPBoldFontMask) { headerString += @"\\b"; trailerString += @"\\b0"; } if (first) currentFont = font; } else if ([currAttrib isEqualToString:CPForegroundColorAttributeName]) { var color = [attributes objectForKey:CPForegroundColorAttributeName]; if (![color isEqual:fgColor]) { headerString += [CPString stringWithFormat:@"\\cf%d", [self numberForColor:color]]; trailerString += @"\\cf0"; } } else if ([currAttrib isEqualToString:CPBackgroundColorAttributeName]) { var color = [attributes objectForKey:CPBackgroundColorAttributeName]; if (![color isEqual:bgColor]) { headerString += [CPString stringWithFormat:@"\\cb%d", [self numberForColor:color]]; trailerString += @"\\cb0"; } } else if ([currAttrib isEqualToString:CPUnderlineStyleAttributeName]) { headerString += @"\\ul"; trailerString += @"\\ulnone "; // trailing space important! } else if ([currAttrib isEqualToString:CPSuperscriptAttributeName]) { var value = [attributes objectForKey:CPSuperscriptAttributeName], ivalue = [value intValue]; if (ivalue > 0) { headerString += @"\\super"; trailerString += @"\\nosupersub "; // trailing space important! } else if (ivalue < 0) { headerString += @"\\sub"; trailerString += @"\\nosupersub "; // trailing space important! } } else if ([currAttrib isEqualToString:CPBaselineOffsetAttributeName]) { var value = [attributes objectForKey:CPBaselineOffsetAttributeName], fvalue = [value floatValue], svalue = Math.round(fvalue * 2.0); // Convert standard points to RTF half-points if (svalue > 0) { headerString += [CPString stringWithFormat:@"\\up%d", svalue]; trailerString += @"\\up0"; } else if (svalue < 0) { // Correct negative formatting using safe positive boundary headerString += [CPString stringWithFormat:@"\\dn%d", Math.abs(svalue)]; trailerString += @"\\dn0"; } } else if ([currAttrib isEqualToString:CPAttachmentAttributeName]) { } else if ([currAttrib isEqualToString:CPLigatureAttributeName]) { } else if ([currAttrib isEqualToString:CPKernAttributeName]) { } } substring = substring.replace(/\\/g, '\\\\'); substring = substring.replace(/\n/g, '\\par\n'); substring = substring.replace(/\t/g, '\\tab '); substring = substring.replace(/{/g, '\\{'); substring = substring.replace(/}/g, '\\}'); if (!first) { var braces; if ([headerString length]) braces = [CPString stringWithFormat:@"{%@ %@}", headerString, substring]; else braces = substring; result += braces; } else { var nobraces; if ([headerString length]) nobraces = [CPString stringWithFormat:@"%@ %@", headerString, substring]; else nobraces = substring; result += nobraces; } return result + trailerString; } - (CPString)bodyString { var string = [text string], result = "", loc = 0, length = [string length], currRange = CPMakeRange(loc, 0), completeRange = CPMakeRange(0, length), paragraphStart = YES; while (CPMaxRange(currRange) < CPMaxRange(completeRange)) { var attributes, substring, runString; attributes = [text attributesAtIndex:CPMaxRange(currRange) longestEffectiveRange:currRange inRange:completeRange]; substring = [string substringWithRange:currRange]; runString = [self runStringForString:substring attributes:attributes paragraphStart:paragraphStart]; result += runString; if (substring.length > 0 && substring.charAt(substring.length - 1) === '\n') paragraphStart = YES; else paragraphStart = NO; } return result; } - (CPString)RTFDStringFromAttributedString:(CPAttributedString)aText documentAttributes:(CPDictionary)dict { var output = [CPString string], headerString, trailerString, bodyString; text = aText; docDict = dict; bodyString = [self bodyString]; trailerString = [self trailerString]; headerString = [self headerString]; output += headerString; output += bodyString; output += trailerString; return output; } @end