From b1fe2612c62057b5ab95beb98e992d9513d876a5 Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Mon, 20 Jan 2025 18:20:09 +0300 Subject: [PATCH 1/7] fixing issue with surrogate pairs, #39 (cherry picked from commit 70a1e75d5a7213cb848c4ddf94ceeb24fe10c44b) (cherry picked from commit a1412e4a2145efba434bf8866ea93c0f6fa2c93b) --- .../org/apache/fop/layoutmgr/inline/TextLayoutManager.java | 4 +++- fop-core/src/main/java/org/apache/fop/util/CharUtilities.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java b/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java index 4b48cd43304..04a045070e5 100644 --- a/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java +++ b/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java @@ -779,6 +779,7 @@ public List getNextKnuthElements(final LayoutContext context, fin boolean inWord = false; boolean inWhitespace = false; char ch = 0; + char prevChar = 0; int level = -1; int prevLevel = -1; boolean retainControls = false; @@ -819,7 +820,7 @@ public List getNextKnuthElements(final LayoutContext context, fin boolean processWord = breakOpportunity || GlyphMapping.isSpace(ch) || CharUtilities.isExplicitBreak(ch) - || ((prevLevel != -1) && (level != prevLevel)); + || ((prevLevel != -1) && (level != prevLevel) && !Character.isHighSurrogate(prevChar)); if (!processWord && foText.getCommonFont().getFontSelectionStrategy() == EN_CHARACTER_BY_CHARACTER) { if (lastFont == null || lastFontPos != nextStart - 1) { lastFont = FontSelector.selectFontForCharactersInText( @@ -887,6 +888,7 @@ public List getNextKnuthElements(final LayoutContext context, fin inWhitespace = ch == CharUtilities.SPACE && foText.getWhitespaceTreatment() != Constants.EN_PRESERVE; prevLevel = level; + prevChar = ch; nextStart++; } diff --git a/fop-core/src/main/java/org/apache/fop/util/CharUtilities.java b/fop-core/src/main/java/org/apache/fop/util/CharUtilities.java index 4be495209b1..27e2291ba79 100644 --- a/fop-core/src/main/java/org/apache/fop/util/CharUtilities.java +++ b/fop-core/src/main/java/org/apache/fop/util/CharUtilities.java @@ -409,7 +409,7 @@ public static boolean containsSurrogatePairAt(CharSequence chars, int index) { char ch = chars.charAt(index); if (Character.isHighSurrogate(ch)) { - if ((index + 1) > chars.length()) { + if ((index + 1) >= chars.length()) { throw new IllegalArgumentException( "ill-formed UTF-16 sequence, contains isolated high surrogate at end of sequence"); } From 37237f3c17d953cb8a19fe8531c927a8da1d557a Mon Sep 17 00:00:00 2001 From: Alexander Dyuzhev Date: Thu, 14 May 2026 19:22:59 +0300 Subject: [PATCH 2/7] TextLayoutManager updated for surrogate pairs issue fix, #39 (cherry picked from commit ba2ec2ea4330af13922a8b75deb0befcc29d66f1) Reflowed on cherry-pick: the guarded condition ran to 157 columns, over checkstyle's 120. The surrogate test is now first, ahead of the font selection strategy lookup. Same result, both operands being pure. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit 3612799243ee859f47ad992f9d8f4c9392e19ef7) --- .../org/apache/fop/layoutmgr/inline/TextLayoutManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java b/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java index 04a045070e5..d4b998f7819 100644 --- a/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java +++ b/fop-core/src/main/java/org/apache/fop/layoutmgr/inline/TextLayoutManager.java @@ -821,7 +821,8 @@ public List getNextKnuthElements(final LayoutContext context, fin || GlyphMapping.isSpace(ch) || CharUtilities.isExplicitBreak(ch) || ((prevLevel != -1) && (level != prevLevel) && !Character.isHighSurrogate(prevChar)); - if (!processWord && foText.getCommonFont().getFontSelectionStrategy() == EN_CHARACTER_BY_CHARACTER) { + if (!processWord && !Character.isHighSurrogate(prevChar) + && foText.getCommonFont().getFontSelectionStrategy() == EN_CHARACTER_BY_CHARACTER) { if (lastFont == null || lastFontPos != nextStart - 1) { lastFont = FontSelector.selectFontForCharactersInText( foText, nextStart - 1, nextStart, foText, this); From 0a9e1a518eda14d691e2f9720b166ca3e9913f8a Mon Sep 17 00:00:00 2001 From: Jason Harrop Date: Fri, 25 Sep 2026 11:01:48 +1000 Subject: [PATCH 3/7] FOP-2918: A high surrogate at the end of a sequence raises IllegalArgumentException containsSurrogatePairAt documents IllegalArgumentException for ill-formed UTF-16, but its end-of-sequence guard read (index + 1) > length, which is never true at the last index. The isolated-surrogate branch was therefore unreachable there and charAt(index + 1) raised StringIndexOutOfBoundsException instead. The existing malformed-sequence test puts the high surrogate mid-string, so it exercised the other branch and passed either way. This one puts it last, and fails with StringIndexOutOfBoundsException before the guard is fixed. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit f62f18b393e611aa0d00a1c0584dcf9ec82081d5) --- .../org/apache/fop/util/CharUtilitiesTestCase.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java b/fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java index b10ae8aea56..82b8491cca1 100644 --- a/fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java +++ b/fop-core/src/test/java/org/apache/fop/util/CharUtilitiesTestCase.java @@ -69,4 +69,14 @@ public void testContainsSurrogatePairAtWithMalformedUTF8Sequence() { CharUtilities.containsSurrogatePairAt(malformedUTF8Sequence, 3); } + + /** A high surrogate as the last character is ill-formed UTF-16, so the documented + * IllegalArgumentException is what callers must see - not the IndexOutOfBoundsException + * that reading one past the end would raise. */ + @Test(expected = IllegalArgumentException.class) + public void testContainsSurrogatePairAtWithIsolatedHighSurrogateAtEndOfSequence() { + String isolatedHighSurrogateAtEnd = "012\uD83D"; + + CharUtilities.containsSurrogatePairAt(isolatedHighSurrogateAtEnd, 3); + } } From 567a26afbace7d38a79bf91f29bfaf5c839e26b6 Mon Sep 17 00:00:00 2001 From: Jason Harrop Date: Fri, 25 Sep 2026 11:51:27 +1000 Subject: [PATCH 4/7] FOP-2918: A non-BMP character under per-character font selection renders Covers the reported failure end to end, which the CharUtilities test does not: that one exercises a latent bug on a different path. With font-selection-strategy="character-by-character", TextLayoutManager ended the word wherever the per-character selection changed font. When the font changed at a surrogate pair, the word was cut between the high surrogate and its low surrogate, and MultiByteFont.mapCharsToGlyphs rejected the fragment with "ill-formed UTF-16 sequence, contains isolated high surrogate at end of sequence". No PDF was produced. MultiByteFont's guard is right; the word splitting was wrong. The case uses Helvetica with Aegean600 as fallback and U+10300, so the font changes exactly at the pair. Aegean600 is already in FOP's test resources, so no new binary ships. Verified to fail before the fix with that exact exception, at MultiByteFont.java:695 by way of GlyphMapping.processWordMapping. Co-Authored-By: Claude Opus 5 (1M context) (cherry picked from commit b64dfe407ec26094795f7557c2a52d2c42cb3074) --- .../fop/render/pdf/PDFEncodingTestCase.java | 20 ++++++++++ .../test-non-bmp-character-by-character.fo | 40 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 fop/test/xml/pdf-encoding/test-non-bmp-character-by-character.fo diff --git a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java index eb2024c27d6..3b13951e96f 100644 --- a/fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java +++ b/fop-core/src/test/java/org/apache/fop/render/pdf/PDFEncodingTestCase.java @@ -117,6 +117,26 @@ public void testPDFEncodingWithNonBMPFont() throws Exception { runTest("test-custom-non-bmp-font.fo", testPatterns); } + /** + * Test a non-BMP character under per-character font selection. The selected font changes at the + * surrogate pair, which must not end the word between the high surrogate and its low surrogate. + * Before that was fixed, layout raised IllegalArgumentException and no PDF was produced. + * + * @throws Exception + * checkstyle wants a comment here, even a silly one + */ + @Test + public void testPDFEncodingWithNonBMPFontCharacterByCharacter() throws Exception { + + final String[] testPatterns = { + TEST_MARKER + "1", "\uD800\uDF00", + TEST_MARKER + "2", "\uD800\uDF00", + TEST_MARKER + "3", "\uD800\uDF00", + }; + + runTest("test-non-bmp-character-by-character.fo", testPatterns); + } + /** Test encoding using specified input file and test patterns array */ private void runTest(String inputFile, String[] testPatterns) throws Exception { diff --git a/fop/test/xml/pdf-encoding/test-non-bmp-character-by-character.fo b/fop/test/xml/pdf-encoding/test-non-bmp-character-by-character.fo new file mode 100644 index 00000000000..0e026987cae --- /dev/null +++ b/fop/test/xml/pdf-encoding/test-non-bmp-character-by-character.fo @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + PDFE_TEST_MARK_1: pair between words 𐌀 here + PDFE_TEST_MARK_2: pair last in the block 𐌀 + PDFE_TEST_MARK_3: consecutive pairs 𐌀𐌀𐌀 + + + From 18b437066ad6d90db2bf270998c9c3aed74378f0 Mon Sep 17 00:00:00 2001 From: Jason Harrop Date: Sat, 3 Oct 2026 07:15:28 +1000 Subject: [PATCH 5/7] FOP-2918: Both units of a surrogate pair resolve to one bidi level UnicodeBidiAlgorithm.resolveLevels(CharSequence, Direction) converts the text to scalar values, leaving a placeholder in the slot of each low surrogate, and its javadoc says the two members of a pair come back with the same level. They did not: no rule resolves the placeholder's class, so it stayed at the embedding level while its character took the level of its script. A supplementary-plane character of a right-to-left script, U+10826 (Cypriot) for one, came back as levels 1 and 0. That is the root of this issue. TextLayoutManager ends a word where the bidi level changes, so it ended one between the two surrogates, and MultiByteFont rejected the fragment with "ill-formed UTF-16 sequence, contains isolated high surrogate at end of sequence". With the guard added earlier on this branch the word is kept whole, and then the line's reordering asserts in InlineRun.split ("heterogeneous inlines not yet supported") because the one word still carries two levels. After resolution, each placeholder now takes the level of the character before it, which is what the javadoc promises. SurrogatePairLevelsTestCase holds it for a pair alone, between Latin letters, and two pairs together. Co-Authored-By: Claude Fable 5.1 --- .../bidi/UnicodeBidiAlgorithm.java | 12 ++++- .../bidi/SurrogatePairLevelsTestCase.java | 52 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 fop-core/src/test/java/org/apache/fop/complexscripts/bidi/SurrogatePairLevelsTestCase.java diff --git a/fop-core/src/main/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java b/fop-core/src/main/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java index 9857ae8f719..f2e449423c9 100644 --- a/fop-core/src/main/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java +++ b/fop-core/src/main/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java @@ -75,7 +75,17 @@ public static int[] resolveLevels(CharSequence cs, Direction defaultLevel) { * @param levels array to receive levels, one for each character in chars array */ public static int[] resolveLevels(int[] chars, int defaultLevel, int[] levels) { - return resolveLevels(chars, getClasses(chars), defaultLevel, levels, false); + resolveLevels(chars, getClasses(chars), defaultLevel, levels, false); + // The placeholder that stands for the low surrogate of a pair takes the level of the + // character it belongs to, as resolveLevels(CharSequence, Direction) documents. No rule + // resolves the placeholder's class, so it was left at the embedding level, and a + // supplementary-plane character of a right-to-left script came out with two levels. + for (int i = 1, n = chars.length; i < n; i++) { + if (chars [ i ] < 0) { + levels [ i ] = levels [ i - 1 ]; + } + } + return levels; } /** diff --git a/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/SurrogatePairLevelsTestCase.java b/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/SurrogatePairLevelsTestCase.java new file mode 100644 index 00000000000..3184c79be96 --- /dev/null +++ b/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/SurrogatePairLevelsTestCase.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (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.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* $Id$ */ + +package org.apache.fop.complexscripts.bidi; + +import org.junit.Test; +import static org.junit.Assert.assertArrayEquals; + +import org.apache.fop.traits.Direction; + +/** + * Both UTF-16 units of a supplementary-plane character must resolve to one bidi level, as + * {@link UnicodeBidiAlgorithm#resolveLevels(CharSequence, Direction)} documents. U+10826 is + * a Cypriot syllable, a right-to-left script outside the BMP (FOP-2918). + */ +public class SurrogatePairLevelsTestCase { + + private static final String CYPRIOT = "ЁРаж"; + + @Test + public void testPairAlone() { + assertArrayEquals(new int[] {1, 1}, UnicodeBidiAlgorithm.resolveLevels(CYPRIOT, Direction.LR)); + } + + @Test + public void testPairBetweenLatinLetters() { + assertArrayEquals(new int[] {0, 1, 1, 0}, + UnicodeBidiAlgorithm.resolveLevels("a" + CYPRIOT + "b", Direction.LR)); + } + + @Test + public void testTwoPairs() { + assertArrayEquals(new int[] {1, 1, 1, 1}, + UnicodeBidiAlgorithm.resolveLevels(CYPRIOT + CYPRIOT, Direction.LR)); + } +} From 6f80646f6579f9bb3cfb7a70d0cc784f07ddaca1 Mon Sep 17 00:00:00 2001 From: Jason Harrop Date: Sat, 3 Oct 2026 07:13:27 +1000 Subject: [PATCH 6/7] FOP-2918: The layout test from the issue's patch: a right-to-left surrogate pair is laid out wordbreak_surrogates.xml is taken unchanged from 2918.patch, attached to FOP-2918 by kwilkerson on 2020-07-17. It sets U+10826 (Cypriot syllabary, a right-to-left script outside the BMP). Before this branch it fails with "ill-formed UTF-16 sequence, contains isolated high surrogate at end of sequence"; with the word-splitting guards alone it gets further and fails on the assertion in InlineRun.split; with the bidi levels of the pair made equal it passes. The TextLayoutManager and CharUtilities changes in that patch are the same as the two earliest commits here, which came by way of the Metanorma fork. Test-by: kwilkerson (FOP-2918) Co-Authored-By: Claude Fable 5.1 --- .../wordbreak_surrogates.xml | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 fop/test/layoutengine/standard-testcases/wordbreak_surrogates.xml diff --git a/fop/test/layoutengine/standard-testcases/wordbreak_surrogates.xml b/fop/test/layoutengine/standard-testcases/wordbreak_surrogates.xml new file mode 100644 index 00000000000..7b6c33a615a --- /dev/null +++ b/fop/test/layoutengine/standard-testcases/wordbreak_surrogates.xml @@ -0,0 +1,47 @@ + + + + + +

+ This test checks that a right to left surrogate pair does not get + word broken in the middle causing an exception. This is for issue FOP-2918 + java.lang.IllegalArgumentException: ill-formed UTF-16 sequence, + contains isolated high surrogate at end of sequence. +

+
+ + + + + + + + + + + 𐠦 + + + + + + + + +
From b401de0d97cc9c5bcb067027fdc02d924c90c7ac Mon Sep 17 00:00:00 2001 From: Jason Harrop Date: Sat, 3 Oct 2026 08:51:31 +1000 Subject: [PATCH 7/7] FOP-2918: A low surrogate's placeholder takes its character's bidi class getClasses gave the placeholder in a low surrogate's slot a class of its own, SURROGATE, which is neither strong, nor neutral, nor retained formatting. Rule N1's look-ahead stops at it, so a neutral outside the BMP inside right-to-left text never saw the strong text after it and fell to the embedding direction. U+1F300 between two Hebrew words in a left-to-right paragraph resolved to level 0, cutting the right-to-left run in two, where U+263A in the same place resolves to 1. Copying the level after resolution, as the earlier commit on this branch does, cannot mend that: the level it copies is already wrong. The placeholder now takes the class of the character it belongs to, so the rules see the pair as one character. Outside the BMP Unicode assigns no ES, CS, WS, S, B or explicit embedding class, so a repeated class resolves as a single one under every rule; the level copy stays as the guarantee the javadoc makes. SurrogatePairLevelsTestCase gains the neutral case (with and without a space before it, beside the same text with U+263A) and N2 between the two directions. The neutral case fails on the level copy alone (element 4, expected 1, was 0). U+1F300 rather than U+1F600 because BidiClass was generated from Unicode data older than 6.1 and reads U+1F600 as L, a separate matter. Co-Authored-By: Claude Opus 5.5 --- .../bidi/UnicodeBidiAlgorithm.java | 13 ++++++-- .../bidi/SurrogatePairLevelsTestCase.java | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/fop-core/src/main/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java b/fop-core/src/main/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java index f2e449423c9..0096476d6d9 100644 --- a/fop-core/src/main/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java +++ b/fop-core/src/main/java/org/apache/fop/complexscripts/bidi/UnicodeBidiAlgorithm.java @@ -77,9 +77,9 @@ public static int[] resolveLevels(CharSequence cs, Direction defaultLevel) { public static int[] resolveLevels(int[] chars, int defaultLevel, int[] levels) { resolveLevels(chars, getClasses(chars), defaultLevel, levels, false); // The placeholder that stands for the low surrogate of a pair takes the level of the - // character it belongs to, as resolveLevels(CharSequence, Direction) documents. No rule - // resolves the placeholder's class, so it was left at the embedding level, and a - // supplementary-plane character of a right-to-left script came out with two levels. + // character it belongs to, as resolveLevels(CharSequence, Direction) documents. With the + // placeholder classed as its character (getClasses) the rules already agree; this holds + // the promise whatever a rule does with a repeated class. for (int i = 1, n = chars.length; i < n; i++) { if (chars [ i ] < 0) { levels [ i ] = levels [ i - 1 ]; @@ -628,6 +628,13 @@ private static int[] getClasses(int[] chars) { int ch = chars [ i ]; if (ch >= 0) { bc = BidiClass.getBidiClass(chars [ i ]); + } else if (i > 0) { + // The placeholder for a low surrogate takes the class of the character it belongs + // to, so that the rules see the pair as the one character it is. As a class of its + // own it ended a run of neutrals in rule N1, so a neutral outside the BMP inside + // right-to-left text fell to the embedding direction where one in the BMP takes the + // text's. + bc = classes [ i - 1 ]; } else { bc = SURROGATE; } diff --git a/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/SurrogatePairLevelsTestCase.java b/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/SurrogatePairLevelsTestCase.java index 3184c79be96..a0c7dcd0ac5 100644 --- a/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/SurrogatePairLevelsTestCase.java +++ b/fop-core/src/test/java/org/apache/fop/complexscripts/bidi/SurrogatePairLevelsTestCase.java @@ -33,6 +33,13 @@ public class SurrogatePairLevelsTestCase { private static final String CYPRIOT = "ЁРаж"; + /** U+1F300 CYCLONE, a neutral (ON) outside the BMP that FOP's bidi class table knows as one. */ + private static final String CYCLONE = "\uD83C\uDF00"; + + private static final String SHALOM = "\u05E9\u05DC\u05D5\u05DD"; + + private static final String OLAM = "\u05E2\u05D5\u05DC\u05DD"; + @Test public void testPairAlone() { assertArrayEquals(new int[] {1, 1}, UnicodeBidiAlgorithm.resolveLevels(CYPRIOT, Direction.LR)); @@ -49,4 +56,29 @@ public void testTwoPairs() { assertArrayEquals(new int[] {1, 1, 1, 1}, UnicodeBidiAlgorithm.resolveLevels(CYPRIOT + CYPRIOT, Direction.LR)); } + + /** + * A neutral outside the BMP inside right-to-left text resolves as a neutral in the BMP does + * (U+263A here): it takes the text's direction by rule N1, so the run is not cut in two. The + * placeholder for the low surrogate, as a class of its own, ended the run of neutrals, and the + * pair fell to the embedding direction; copying the level after resolution cannot mend that. + */ + @Test + public void testNeutralPairInsideRightToLeftText() { + assertArrayEquals(new int[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + UnicodeBidiAlgorithm.resolveLevels(SHALOM + "\u263A " + OLAM, Direction.LR)); + assertArrayEquals(new int[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + UnicodeBidiAlgorithm.resolveLevels(SHALOM + CYCLONE + " " + OLAM, Direction.LR)); + assertArrayEquals(new int[] {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, + UnicodeBidiAlgorithm.resolveLevels(SHALOM + " " + CYCLONE + " " + OLAM, Direction.LR)); + } + + /** Between left-to-right and right-to-left text the same neutral takes the embedding direction (N2). */ + @Test + public void testNeutralPairBetweenDirections() { + assertArrayEquals(new int[] {0, 0, 0, 0, 0, 1, 1, 1, 1}, + UnicodeBidiAlgorithm.resolveLevels("ab" + CYCLONE + " " + OLAM, Direction.LR)); + assertArrayEquals(new int[] {2, 2, 1, 1, 1, 1, 1, 1, 1}, + UnicodeBidiAlgorithm.resolveLevels("ab" + CYCLONE + " " + OLAM, Direction.RL)); + } }