{"id":111858,"date":"2025-12-11T07:00:00","date_gmt":"2025-12-11T15:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=111858"},"modified":"2025-12-19T12:21:48","modified_gmt":"2025-12-19T20:21:48","slug":"20251211-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20251211-00\/?p=111858","title":{"rendered":"Studying the various locale mismatch scenarios in Windows clipboard text format synthesis"},"content":{"rendered":"<p>So far, we&#8217;ve learned that the conversion between Unicode and the 8-bit ANSI and OEM code pages is performed with the assistance of the <code>CF_<wbr \/>LOCALE<\/code> clipboard format, which itself <a title=\"How does Windows synthesize the CF_LOCALE clipboard format?\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20251210-00\/?p=111856\"> comes from the active keyboard layout<\/a>. We left with the question of whether this is the right thing, giving as an example the case of highlighting some text in Hebrew and copying it to the clipboard. Shouldn&#8217;t that be set with a Hebrew LCID?<\/p>\n<p>First of all, you have to specify what you mean by &#8220;copy it to the clipboard.&#8221; Suppose the English-language user selected some Hebrew text and the program set it to the clipboard as <code>CF_<wbr \/>UNICODE\u00adTEXT<\/code> with a <i>Hebrew<\/i> LCID. A program which reads the <code>CF_<wbr \/>UNICODE\u00adTEXT<\/code> will read the original Unicode text, with Hebrew characters intact. The LCID plays no role since no conversion was performed. So in the case where the string was placed as Unicode and retrieved as Unicode, everything is fine.<\/p>\n<p>If the string were placed as Unicode but read as <code>CF_<wbr \/>TEXT<\/code>, the retrieving program will get the string translated to code page 1252, since that is the ANSI code page used by the US-English LCID. Is this the correct code page? Well, if the retrieving program is using <code>CF_<wbr \/>TEXT<\/code>, then it is a program that uses the 8-bit ANSI character set as its string encoding, and if you&#8217;re running on a US-English system, then the 8-bit ANSI character set is code page 1252. So translating the Hebrew text to ANSI via code page 1252 is correct. You need to translate the string into the ANSI code page that the retrieving program is using.<\/p>\n<p>Conversely, if the Hebrew string were placed on the clipboard as 8-bit ANSI in code page 1252, then\u2026 wait, that&#8217;s a trick question! Code page 1252 doesn&#8217;t have any Hebrew characters! If a program uses the US-English 8-bit ANSI character set, it cannot represent Hebrew characters at all, so the scenario itself is flawed: There can&#8217;t be any Hebrew text on the screen to be selected since the program has no way of displaying it.<\/p>\n<p>Now, I guess it could be possible if a program internally supported enough Unicode to display Hebrew characters, but still chose to put text on the clipboard in ANSI format. But in that case, it would be putting question marks on the clipboard since there are no Hebrew characters in code page 1252. Any program that does this intentionally is clearly being pathological: Why do all the work to display characters in Unicode, yet copy those character to the clipboard in 8-bit ANSI?<\/p>\n<p>But wait, let&#8217;s rewind to a simpler scenario where there are no character set conversions at all. A program sets text on the clipboard in 8-bit ANSI, and another program reads it. If we consult our table, we see that the entry for this is &#8220;N\/A&#8221;: There is no conversion. This holds true even if the program that put the text on the clipboard and the program that reads the text from the clipboard disagree on what the 8-bit ANSI code page is.<\/p>\n<p>Prior to the introduction of <a title=\"The activeCodePage manifest element can be used for more than just setting UTF-8 as the active code page\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20220531-00\/?p=106697\"> the activeCodePage manifest declaration<\/a>, the identity of the 8-bit ANSI code page was the same for all applications running in the same desktop. There was no opportunity for mismatch, so if one program put the text on the clipboard in 8-bit ANSI, and another read it out in 8-bit ANSI, they necessarily agreed on what the 8-bit ANSI code page was, since there was only one. But now that we have the ability for different programs to have a different value for the 8-bit ANSI code page, this nop-transformation will result in mojibake if the reader and writer have different ideas about what the 8-bit ANSI code page is.<\/p>\n<p>You have the same problem with the <code>Ansi\u00adTo\u00adOem<\/code> conversion: Historically, all programs agreed on what the 8-bit ANSI and 8-bit OEM code pages are, so the system maintains a single &#8220;ANSI\u21c6OEM&#8221; conversion table that is shared by all processes. But now that programs can choose (indirectly) their ANSI and OEM code pages, you have a problem if those choices don&#8217;t match those the system would have chosen.<\/p>\n<p>The people who added <code>activeCodePage<\/code> support hooked it up to the <code>GetACP()<\/code> and <code>GetOEMCP()<\/code> functions, as well as the to the A-suffixed functions which convert their 8-bit ANSI string parameters to Unicode before forwarding the result to the W-suffixed functions. But there are other places that didn&#8217;t get updated because doing so would require larger architectural changes, would affect performance of programs that didn&#8217;t use the <code>activeCodePage<\/code> feature, would introduce regression risk, and could lead to compatibility problems. Not saying that they couldn&#8217;t have done it, but it would have taken longer, and maybe it&#8217;s better to have a good-enough feature than a perfect one.<\/p>\n<p>While doing fact-checking on this series of articles, I wrote some test programs that tried to trigger the <code>CF_<wbr \/>TEXT<\/code>-to-<code>CF_<wbr \/>OEM\u00adTEXT<\/code> conversion, and they didn&#8217;t behave as I expected.<\/p>\n<pre>\/\/ Note: Test program doesn't do error-checking.\r\n\r\n\/\/ Put the ANSI string \"\\xD0\\x00\" on the clipboard,\r\n\/\/ with the locale 1049 (ru-ru).\r\nint main()\r\n{\r\n    if (OpenClipboard(hwnd)) {\r\n        EmptyClipboard();\r\n\r\n        \/\/ Put an ANSI string on the clipboard.\r\n        HGLOBAL glob = GlobalAlloc(GMEM_MOVEABLE, 2);\r\n        PSTR message = (PSTR)GlobalLock(glob);\r\n        message[0] = 0xD0;\r\n        message[1] = 0x00;\r\n        GlobalUnlock(glob);\r\n        SetClipboardData(CF_TEXT, glob);\r\n\r\n        \/\/ Mark it as locale 0x0419 = 1049 = ru-ru\r\n        glob = GlobalAlloc(GMEM_MOVEABLE, sizeof(LCID));\r\n        *(LCID*)GlobalLock(glob) = 0x0419;\r\n        GlobalUnlock(glob);\r\n        SetClipboardData(CF_LOCALE, glob);\r\n\r\n        CloseClipboard();\r\n    }\r\n}\r\n<\/pre>\n<p>And here&#8217;s the program to read the string back out in the OEM code page.<\/p>\n<pre>int main()\r\n{\r\n    if (OpenClipboard(hwnd)) {\r\n        HGLOBAL glob = GetClipboardData(CF_OEMTEXT);\r\n        PSTR message = (PSTR)GlobalLock(glob);\r\n        printf(\"%0x02x\\n\", message[0]);\r\n        GlobalUnlock(glob);\r\n\r\n        CloseClipboard();\r\n    }\r\n}\r\n<\/pre>\n<p>I ran this on a US-English system, so the LCID is <code>0x0409<\/code> = 1033, the ANSI code page is <a href=\"https:\/\/en.wikipedia.org\/wiki\/Windows-1252\">1252<\/a>, and the OEM code page is <a href=\"https:\/\/en.wikipedia.org\/wiki\/Code_page_437\">437<\/a>. The character <tt>D0<\/tt> in code page 1252 is \u00d0 = U+00D0. This character does not exist in code page 437, so <code>Ansi\u00adTo\u00adOem<\/code> uses the best-fit character D = U+0044, which is in position <tt>44<\/tt> in code page 437.<\/p>\n<p>When I ran this program, I expected the <code>CF_<wbr \/>OEM\u00adTEXT<\/code> string to have the byte <tt>44<\/tt>, but it didn&#8217;t. It had the byte <tt>90<\/tt>. We will start unraveling this mystery next time.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If they don&#8217;t match, then the 8-bit strings are basically broken already.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-111858","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>If they don&#8217;t match, then the 8-bit strings are basically broken already.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111858","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=111858"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/111858\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=111858"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=111858"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=111858"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}