view dynamin/painting/windows_text_layout.d @ 102:604d20cac836

Add dynamin.core.array and put contains() there.
author Jordan Miner <jminer7@gmail.com>
date Tue, 15 May 2012 21:14:48 -0500
parents 301e077da540
children 73060bc3f004
line wrap: on
line source

// 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;

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());
	}
}