diff dynamin/painting/windows_text_layout.d @ 96:301e077da540

Add the beginnings of a Windows Uniscribe text backend.
author Jordan Miner <jminer7@gmail.com>
date Wed, 02 May 2012 03:19:00 -0500
parents
children 604d20cac836
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dynamin/painting/windows_text_layout.d	Wed May 02 03:19:00 2012 -0500
@@ -0,0 +1,497 @@
+// Written in the D programming language
+// www.digitalmars.com/d/
+
+/*
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is the Dynamin library.
+ *
+ * The Initial Developer of the Original Code is Jordan Miner.
+ * Portions created by the Initial Developer are Copyright (C) 2007-2012
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Jordan Miner <jminer7@gmail.com>
+ *
+ */
+
+module dynamin.painting.windows_text_layout;
+
+public import dynamin.c.cairo;
+public import dynamin.c.cairo_win32;
+public import dynamin.c.windows;
+public import dynamin.c.uniscribe;
+public import dynamin.painting.graphics;
+public import tango.io.Stdout;
+public import tango.text.convert.Utf;
+public import tango.math.Math;
+
+//version = TextLayoutDebug;
+
+// TODO: move to array.d
+bool containsT(T)(T[] array, T item) {
+	foreach(T item2; array) {
+		if(item == item2)
+			return true;
+	}
+	return false;
+}
+template TextLayoutBackend() {
+	uint charToWcharIndex(uint index) {
+		uint wcharIndex = 0;
+		foreach(wchar c; text[0..index])
+			wcharIndex++;
+		return wcharIndex;
+	}
+	uint wcharToCharIndex(uint wcharIndex) {
+		uint decoded = 0, ate = 0, index = 0;
+		while(decoded < wcharIndex) {
+			decoded += (decode(text[index..$], ate) <= 0xFFFF ? 1 : 2);
+			index += ate;
+		}
+		return index;
+	}
+
+	uint nextRight(uint index) {
+		uint wIndex = charToWcharIndex(index);
+		// this requires using the output from ScriptLayout to move in visual order
+		return 0;
+	}
+
+	//{{{ font fallback
+	static {
+		int Latin;
+		int Greek;
+		int Cyrillic;
+		int Armenian;
+		int Georgian;
+		int Hebrew;
+		int Arabic;
+		int Syriac;
+		int Thaana;
+		int Thai;
+		int Devanagari;
+		int Tamil;
+		int Bengali;
+		int Gurmukhi;
+		int Gujarati;
+		int Oriya;
+		int Telugu;
+		int Kannada;
+		int Malayalam;
+		int Lao;
+		int Tibetan;
+		//int Japanese;
+		int Bopomofo;
+		int CjkIdeographs;
+		int CjkSymbols;
+		int KoreanHangul;
+	}
+	static this() {
+		Latin              = getUniscribeScript("JKL");
+		Greek              = getUniscribeScript("ΠΡΣ");
+		Cyrillic           = getUniscribeScript("КЛМ");
+		Armenian           = getUniscribeScript("ԺԻԼ");
+		Georgian           = getUniscribeScript("ႩႪႫ");
+		Hebrew             = getUniscribeScript("הוז");
+		Arabic             = getUniscribeScript("ثجح");
+		Syriac             = getUniscribeScript("ܕܗܚ");
+		Thaana             = getUniscribeScript("ގޏސ");
+		Thai               = getUniscribeScript("ฆงจ");
+		Devanagari         = getUniscribeScript("जझञ");
+		Tamil              = getUniscribeScript("ஜஞட");
+		Bengali            = getUniscribeScript("জঝঞ");
+		Gurmukhi           = getUniscribeScript("ਜਝਞ");
+		Gujarati           = getUniscribeScript("જઝઞ");
+		Oriya              = getUniscribeScript("ଜଝଞ");
+		Telugu             = getUniscribeScript("జఝఞ");
+		Kannada            = getUniscribeScript("ಜಝಞ");
+		Malayalam          = getUniscribeScript("ജഝഞ");
+		Lao                = getUniscribeScript("ຄງຈ");
+		Tibetan            = getUniscribeScript("ཇཉཊ");
+		//Japanese           = getUniscribeScript("せゼカ"); // TODO: on XP, returned more than 1 item
+		Bopomofo           = getUniscribeScript("ㄐㄓㄗ"); // Chinese
+		CjkIdeographs      = getUniscribeScript("丛东丝");
+		CjkSymbols         = getUniscribeScript("、〜㈬");
+		KoreanHangul       = getUniscribeScript("갠흯㉡");
+	}
+	// returns the Uniscribe script number (SCRIPT_ANALYSIS.eScript) from the specified sample text
+	static int getUniscribeScript(wchar[] sample) {
+		SCRIPT_ITEM[5] items;
+		int itemsProcessed;
+		HRESULT r = ScriptItemize(sample.ptr,
+		                          sample.length,
+		                          items.length-1,
+		                          null,
+		                          null,
+		                          items.ptr,
+		                          &itemsProcessed);
+		assert(r == 0);
+		assert(itemsProcessed == 1);
+		return items[0].a.eScript.get();
+	}
+
+	// adds fallback fonts to the specified fallbacks array
+	void getFontFallbacks(int script, wchar[][] fallbacks) {
+		int i = 0;
+		void addFallback(wchar[] str) {
+			fallbacks[i++] = str;
+		}
+		if(script == Latin || script == Greek || script == Cyrillic)
+			addFallback("Times New Roman");
+		else if(script == Hebrew || script == Arabic)
+			addFallback("Times New Roman");
+		else if(script == Armenian || script == Georgian)
+			addFallback("Sylfaen"); // fits in well with English fonts
+		else if(script == Bengali)
+			addFallback("Vrinda");
+		else if(Devanagari)
+			addFallback("Mangal");
+		else if(script == Gujarati)
+			addFallback("Shruti");
+		else if(script == Gurmukhi)
+			addFallback("Raavi");
+		else if(script == Kannada)
+			addFallback("Tunga");
+		else if(script == Malayalam)
+			addFallback("Kartika");
+		else if(script == Syriac)
+			addFallback("Estrangelo Edessa");
+		else if(script == Tamil)
+			addFallback("Latha");
+		else if(script == Telugu)
+			addFallback("Gautami");
+		else if(script == Thaana)
+			addFallback("MV Boli");
+		else if(script == Thai)
+			addFallback("Tahoma"); // fits in well with English fonts
+		//else if(script == Japanese)
+		//	addFallback("MS Mincho"); // Meiryo doesn't fit in with SimSun...
+		else if(script == Bopomofo || script == CjkIdeographs || script == CjkSymbols)
+			addFallback("SimSun");
+		else if(script == KoreanHangul)
+			addFallback("Batang");
+		else if(script == Oriya)
+			addFallback("Kalinga");    // new with Vista
+		else if(script == Lao)
+			addFallback("DokChampa");  // new with Vista
+		else if(script == Tibetan)
+			addFallback("Microsoft Himalaya"); // new with Vista
+
+		// Arial Unicode MS is not shipped with Windows, but is with Office
+		addFallback("Arial Unicode MS");
+	}
+	//}}}
+
+	/*bool isRightAligned() {
+		return alignment == TextAlignment.Right ||
+		       ((alignment == TextAlignment.Justify || alignment == TextAlignment.Natural) &&
+		        items[0].a.fRTL);
+	}*/
+	/*struct Run {
+		double height;
+		int wtextIndex;
+		int itemIndex;
+		WORD[] logClusters;        // length == length of text as UTF-16
+		WORD[] glyphs;
+		SCRIPT_VISATTR[] visAttrs; // length == glyphs.length
+		int[] advanceWidths;       // length == glyphs.length
+		GOFFSET[] offsets;         // length == glyphs.length
+	}*/
+	// TODO: should save memory if logClusters, glyphs, visAttrs, advanceWidths, & offsets
+	//       were made as one big array and these were just slices of them
+	//       Then reuse the big array every time in layout()
+
+	void backend_splitRun(uint runIndex, uint splitIndex) {
+		visAttrs.insert(visAttrs[runIndex], runIndex);
+		visAttrs[runIndex]   = visAttrs[runIndex][0..runs[runIndex].glyphs.length];
+		visAttrs[runIndex+1] = visAttrs[runIndex+1][runs[runIndex].glyphs.length..$];
+
+		offsets.insert(offsets[runIndex], runIndex);
+		offsets[runIndex]   = offsets[runIndex][0..runs[runIndex].glyphs.length];
+		offsets[runIndex+1] = offsets[runIndex+1][runs[runIndex].glyphs.length..$];
+	}
+	// returns the number of UTF-8 code units (bytes) it takes to encode the
+	// specified UTF-16 code unit; returns 4 for a high surrogate and 0 for a low surrogate
+	int getUtf8Width(wchar c) {
+		if(c >= 0x00 && c <= 0x7F) {
+			return 1;
+		} else if(c >= 0x0080 && c <= 0x07FF) {
+			return 2;
+		} else if(isHighSurrogate(c)) {
+			return 4;
+		} else if(isLowSurrogate(c)) {
+			return 0;
+		} else {
+			return 3;
+		}
+	}
+
+	SCRIPT_ITEM[] items;
+	List!(SCRIPT_VISATTR[]) visAttrs; // one for each Run, same length as Run.glyphs
+	List!(GOFFSET[]) offsets;         // one for each Run, same length as Run.glyphs
+	void backend_preprocess(Graphics g) {
+		wchar[] wtext = toString16(text);
+
+		//{{{ call ScriptItemize() and ScriptBreak()
+		SCRIPT_CONTROL scriptControl;
+		SCRIPT_STATE scriptState;
+		// TODO: digit substitution?
+
+		if(items.length < 50)
+			items.length = 50;
+		int itemsProcessed;
+		// On 7, ScriptItemize returns E_OUTOFMEMORY outright if the 3rd param isn't at least 12.
+		// Every period, question mark, quotation mark, start of number, etc. starts a new item.
+		// A short English sentence can easily have 10 items.
+		while(ScriptItemize(wtext.ptr,
+		                    wtext.length,
+		                    items.length-1,
+		                    &scriptControl,
+		                    &scriptState,
+		                    items.ptr,
+		                    &itemsProcessed) == E_OUTOFMEMORY) {
+			items.length = items.length * 2;
+		}
+		items = items[0..itemsProcessed+1]; // last item is the end of string
+
+		for(int i = 0; i < logAttrs.length; ++i) // clear array from previous use
+			logAttrs[i] = LogAttr.init;
+		logAttrs.length = text.length;
+		int laIndex = 0;
+
+		SCRIPT_LOGATTR[] tmpAttrs;
+		bool lastWhitespace = false;
+		for(int i = 0; i < items.length-1; ++i) {
+			wchar[] itemText = wtext[items[i].iCharPos..items[i+1].iCharPos];
+			tmpAttrs.length = itemText.length;
+			HRESULT result = ScriptBreak(itemText.ptr, itemText.length, &items[i].a, tmpAttrs.ptr);
+			if(FAILED(result))
+				throw new Exception("ScriptBreak() failed");
+
+			// ScriptBreak() does not set fSoftBreak for the first character of items, even
+			// when it needs to be (it doesn't know what comes before them...). This loop sets
+			// it for characters after a breakable whitespace.
+			for(int j = 0; j < tmpAttrs.length; ++j) {
+				if(tmpAttrs[j].fWhiteSpace.get()) {
+					lastWhitespace = true;
+				} else {
+					if(lastWhitespace) // not whitespace, but last char was
+						tmpAttrs[j].fSoftBreak.set(true);
+					lastWhitespace = false;
+				}
+
+				// have to convert the SCRIPT_LOGATTR array, which corresponds with the UTF-16
+				// encoding, to the LogAttr array, which corresponds with the UTF-8 encoding
+				if(tmpAttrs[j].fSoftBreak.get())
+					logAttrs[laIndex].softBreak = true;
+				if(tmpAttrs[j].fCharStop.get())
+					logAttrs[laIndex].clusterStart = true;
+				if(tmpAttrs[j].fWordStop.get())
+					logAttrs[laIndex].wordStart = true;
+
+				laIndex += getUtf8Width(itemText[j]);
+				if(isHighSurrogate(itemText[j]))
+					j++; // skip the low surrogate
+			}
+		}
+		assert(laIndex == logAttrs.length);
+		//}}}
+
+		// ScriptShape and some other functions need an HDC. If the target surface is win32,
+		// just use its DC. Otherwise, create a 1x1 DIB and use its DC.
+		cairo_surface_t* targetSurface = cairo_get_target(g.handle);
+		cairo_surface_flush(targetSurface);
+		cairo_t* cr = g.handle;
+		HDC hdc = cairo_win32_surface_get_dc(targetSurface);
+		cairo_surface_t* tmpSurface;
+		if(!hdc) {
+			cairo_format_t format = CAIRO_FORMAT_ARGB32;
+			if(cairo_surface_get_type(targetSurface) == CAIRO_SURFACE_TYPE_IMAGE)
+				format = cairo_image_surface_get_format(targetSurface);
+			tmpSurface = cairo_win32_surface_create_with_dib(format, 1, 1);
+			cr = cairo_create(tmpSurface);
+			hdc = cairo_win32_surface_get_dc(tmpSurface);
+			assert(hdc != null);
+		}
+		scope(exit) {
+			if(tmpSurface) {
+				cairo_destroy(cr);
+				cairo_surface_destroy(tmpSurface);
+			}
+		}
+
+		clearRuns(); // releaseScaledFont() must be called when a run is removed
+
+		if(!visAttrs)
+			visAttrs = new List!(SCRIPT_VISATTR[]);
+		visAttrs.clear();
+		if(!offsets)
+			offsets = new List!(GOFFSET[]);
+		offsets.clear();
+		List!(BYTE) levels = new List!(BYTE)(items.length+4); // 4 gives padding for 2 formattings
+
+		SaveDC(hdc);
+		int splitter(uint i) {
+			return i < items.length ? wcharToCharIndex(items[i].iCharPos) : -1;
+		}
+		int itemIndex = 0;
+		// Merge the SCRIPT_ITEMs with runs that have the same format
+		// The only formats that matter here are the ones that affect the size or
+		// shape of characters, so use &fontFormatRuns
+		foreach(start, length, format; fontFormatRuns(&splitter)) {
+			uint wstart = charToWcharIndex(start);
+			uint wend = charToWcharIndex(start+length);
+			if(items[itemIndex+1].iCharPos == wstart)
+				itemIndex++;
+
+			levels.add(items[itemIndex].a.s.uBidiLevel.get());
+
+			cairo_matrix_t ctm;
+			cairo_get_matrix(cr, &ctm);
+			cairo_scaled_font_t* font = getScaledFont(format.fontFamily,
+													  format.fontSize,
+													  format.bold,
+													  format.italic,
+													  ctm);
+			cairo_win32_scaled_font_select_font(font, hdc);
+			// Cairo sets up the HDC to be scaled 32 times larger than stuff will be drawn.
+			// get_metrics_factor returns the factor necessary to convert to font space units.
+			// Font space units need multiplied by the font size to get device units
+			// multipling by to_dev converts from logical units to device units
+			double to_dev = cairo_win32_scaled_font_get_metrics_factor(font) * format.fontSize;
+
+			SCRIPT_CACHE cache = null;
+			scope(exit) ScriptFreeCache(&cache);
+
+			wchar[] range = wtext[wstart..wend];
+
+			WORD[] outGlyphs = new WORD[range.length * 3 / 2 + 16];
+			WORD[] logClust = new WORD[range.length];
+			SCRIPT_VISATTR[] sva = new SCRIPT_VISATTR[outGlyphs.length];
+			int glyphsReturned;
+			do {
+				HRESULT result = ScriptShape(hdc,
+											 &cache,
+											 range.ptr,
+											 range.length,
+											 outGlyphs.length,
+											 &items[itemIndex].a,
+											 outGlyphs.ptr,
+											 logClust.ptr,
+											 sva.ptr,
+											 &glyphsReturned);
+				if(result == E_OUTOFMEMORY) {
+					outGlyphs.length = outGlyphs.length * 3 / 2;
+					sva.length = outGlyphs.length;
+					continue;
+				} else if(result == USP_E_SCRIPT_NOT_IN_FONT) {
+					// TODO: font fallback
+				}
+				/*SCRIPT_FONTPROPERTIES fontProps;
+				ScriptGetFontProperties(hdc, &cache, &fontProps);
+				Stdout("*****Blank: ")(fontProps.wgBlank).newline;
+				Stdout(fontProps.wgDefault).newline;*/
+			} while(false);
+			outGlyphs = outGlyphs[0..glyphsReturned];
+			sva = sva[0..glyphsReturned];
+			visAttrs.add(sva);
+
+			int[] advance = new int[outGlyphs.length];
+			GOFFSET[] goffset = new GOFFSET[outGlyphs.length];
+			// the docs mistakenly say the GOFFSET array is optional,
+			// but it is actually the ABC structure
+			if(FAILED(ScriptPlace(hdc,
+								  &cache,
+								  outGlyphs.ptr,
+								  outGlyphs.length,
+								  sva.ptr,
+								  &items[itemIndex].a,
+								  advance.ptr,
+								  goffset.ptr,
+								  null)))
+				throw new Exception("ScriptPlace failed");
+			for(int i = 0; i < goffset.length; ++i) {
+				goffset[i].du = cast(LONG)(goffset[i].du * to_dev);
+				goffset[i].dv = cast(LONG)(goffset[i].dv * to_dev);
+			}
+			offsets.add(goffset);
+			// TODO: handle errors well
+			// TODO: kerning here
+
+			Run run;
+			run.font = font;
+			cairo_font_extents_t extents;
+			cairo_scaled_font_extents(run.font, &extents);
+			run.height = extents.height;
+
+			run.start = start;
+			run.length = length;
+			run.rightToLeft = items[itemIndex].a.fRTL.get() ? true : false;
+
+			run.clusterMap = new uint[run.length];
+			int clusterIndex = 0;
+			for(int i = 0; i < logClust.length; ++i) {
+				int width = getUtf8Width(range[i]);
+				if(isHighSurrogate(range[i]))
+					i++; // skip the low surrogate
+				for(; width > 0; --width) {
+					run.clusterMap[clusterIndex] = logClust[i];
+					clusterIndex++;
+				}
+			}
+			assert(clusterIndex == run.length);
+
+			run.glyphs = new uint[outGlyphs.length];
+			for(int i = 0; i < outGlyphs.length; ++i)
+				run.glyphs[i] = outGlyphs[i];
+
+			run.advanceWidths = new float[advance.length];
+			for(int i = 0; i < advance.length; ++i)
+				run.advanceWidths[i] = advance[i] * to_dev;
+
+			runs.add(run);
+
+			cairo_win32_scaled_font_done_font(font);
+
+		}
+		RestoreDC(hdc, -1);
+		assert(itemIndex == items.length-2); // last item is end of string
+
+		// fill in visToLogMap
+		visToLogMap = new uint[runs.count];
+		if(FAILED( ScriptLayout(levels.count, levels.data.ptr, cast(int*)visToLogMap.ptr, null) ))
+			throw new Exception("ScriptLayout() failed");
+
+	}
+
+	static cairo_scaled_font_t* backend_createScaledFont(string family,
+	                                                     double size,
+	                                                     bool bold,
+	                                                     bool italic,
+	                                                     cairo_matrix_t* ctm) {
+		LOGFONT lf;
+		lf.lfHeight = -cast(int)size; // is this ignored by cairo? uses font_matrix instead?
+		lf.lfWeight = bold ? 700 : 400;
+		lf.lfItalic = italic;
+		lf.lfCharSet = 1; // DEFAULT_CHARSET
+		auto tmp = toString16(family);
+		lf.lfFaceName[0..tmp.length] = tmp;
+		lf.lfFaceName[tmp.length] = '\0';
+
+		auto face = cairo_win32_font_face_create_for_logfontw(&lf);
+		scope(exit) cairo_font_face_destroy(face);
+
+		cairo_matrix_t font_matrix;
+		cairo_matrix_init_scale(&font_matrix, size, size);
+		return cairo_scaled_font_create(face, &font_matrix, ctm, cairo_font_options_create());
+	}
+}