Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JDK-8293776 : Adds CSS 4 and 8 digits hex coded Color #13

Open
scientificware opened this issue Sep 10, 2022 · 0 comments
Open

JDK-8293776 : Adds CSS 4 and 8 digits hex coded Color #13

scientificware opened this issue Sep 10, 2022 · 0 comments
Assignees
Labels
Author Contributed a patch to fix this issue Enhancement New feature or request Fixed Fixed issue. Reporter Reported this Issue to Java Bug DataBase WIP Work In Progress

Comments

@scientificware
Copy link
Owner

scientificware commented Sep 10, 2022

The purpose of this issue is to add CSS 4 and 8 digits hex coded Color in CSS.java.

This is referenced in Java Bug Database as

This is tracked in JBS as

Related Pull Request

Adds the 4 and 8 digits color hex notations to CSS.java, as described in :
CSS Color Module Level 4
W3C Candidate Recommendation Snapshot, 5 July 2022
4.1 The <Color> syntax
6.2 The RGB Hexadecimal Notations: #RRGGBB

Status

  • This Enhancement Request is not yet accepted so it's internaly referenced in Java Bug Database as JI-9073933.

  • This extends the previous closed JDK-4895924 issue see table below.

  • The present CSS implementation accepts 3 digits or 6 digits hex coded Color as following :

    • With the CSS 1 convention : #rgb must be interpreted as #rrggbb opaque Color.
    • Only the 6 highest digits are parsed.
    • When 1, 2, 4 and 5 digits, they are supposed represent the lowest digits. The missing digits to reach 6 digits are replaced with 0 (This is not a CSS recommendation)
      • Then #f0a is interpreted as #ff00aa opaque Color (Correct).
      • And #f00a is interpreted as #00f00a opaque Color (Wrong).
  • I request to reach CSS Color Level 4 recommendation which accepts 3, 4, 6 and 8 digits.

    • With the following CSS C L 4 conventions:
      • #rgb must be interpreted as #rrggbb opaque Color.
      • #rgba must be interpreted as #rrggbbaa Color with an aa Alpha value.
      • #rrggbb must be interpreted as #rrggbb opaque Color.
      • #rrggbbaa must be interpreted as #rrggbbaa Color with an aa Alpha value.
      • only a maximum of 8 highest digits in the string are parsed. Strings with length = 1, 2, 5, 7 or wrong digits return null to avoid to return a wrong color.
    • For example : #f00a will be interpreted as #ff0000aa.
    • I chose a strict interpretation of the recommendation.

Impacts of these modifications.

  • This PR modifies the CSS.java CopyRight date.
  • This PR modifies CSS.java hexToColor method
    1. by adding two hex format code,
    2. by treating only 2, 3, 4 and 8 digits well formated hex code, all other strings return null.
  • Refactoring rgb method will be treated in another PR. rgba is a legacy format, it would be better to not mention it in documentation even if CSS.java takes it in charge. In fact rgb could be reached trough rgba.
  • Done : Performance impact is not evaluated. Enhancements must be at least without performance cost.
  • To Do : Remove the former comment after decryption :
    // This makes sure that white (#ffffff) can be specified with the short notation
    // (#fff) and removes any dependencies on the color depth of the display.
  • Done : Try to use Integer.rotateRight(int i, int distance)method + Color(int rgba, boolean hasalpha) constructor.
    Test will determine which is the best. Conclusion, none gain in time, only
    private static final Pattern hex = Pattern.compile("\\p{XDigit}+");
    /**
    * Convert a "#FFF", "#FFFF", "#FFFFFF" or "#FFFFFFFF" hex string to a Color.
    * If the color specification is bad, an attempt
    * will be made to fix it up.
    */
    static final Color hexToColor(String digits) {
    int n = digits.length();
    if (digits.startsWith("#")) {
    digits = digits.substring(1, Math.min(n, 9));
    n--;
    }
    // CSS Color level 4
    // - defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
    // - allows, webpage passes 3, 4, 6 or 8 digit color code.
    // - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
    // - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
    // - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
    // - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
    //
    // Be careful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
    // Since this method is defined in CSS.java, it must only take in charge CSS Color Level 4 notations.
    //
    // According notes below the current OpenJDK implementation is
    // - 3 digits #[R][G][B] .......... represents #[RR][GG][BB]FF
    // - 6 digits #[R][G][B] .......... represents #[RR][GG][BB]FF
    //
    // Some webpages pass 3 digit color code as in #fff which is
    // decoded as #000FFF resulting in blue background.
    // As per https://www.w3.org/TR/CSS1/#color-units,
    // The three-digit RGB notation (#rgb) is converted into six-digit form
    // (#rrggbb) by replicating digits, not by adding zeros.
    // This makes sure that white (#ffffff) can be specified with the short notation
    // (#fff) and removes any dependencies on the color depth of the display.
    if (n == 3 && hex.matcher(digits).matches()) {
    final char r = digits.charAt(0);
    final char g = digits.charAt(1);
    final char b = digits.charAt(2);
    digits = String.format("%1$s%1$s%2$s%2$s%3$s%3$sff", r, g, b);
    } else if (n == 4 && hex.matcher(digits).matches()) {
    final char r = digits.charAt(0);
    final char g = digits.charAt(1);
    final char b = digits.charAt(2);
    final char a = digits.charAt(3);
    digits = String.format("%1$s%1$s%2$s%2$s%3$s%3$s%4$s%4$s", r, g, b, a);
    } else if (n == 6 && hex.matcher(digits).matches()) {
    digits += "ff";
    } else if (n != 8 || !hex.matcher(digits).matches()) {
    return null;
    }
    try {
    return new Color(Integer.rotateRight(Integer.parseUnsignedInt(digits, 16),8),
    true);
    } catch (NumberFormatException nfe) {
    return null;
    }
    }
  • Returned value according string argument :
    Value Ret. Val. Before PR Ret. Val. after PR
    null null 🟢
    # null null 🟢
    #f java.awt.Color[r=0,g=0,b=15] + a=255 null
    #f0 java.awt.Color[r=0,g=0,b=240] + a=255 null
    #f0f java.awt.Color[r=255,g=0,b=255] + a=255 java.awt.Color[r=255,g=0,b=255] + a=255 🟢
    #f0f1 java.awt.Color[r=0,g=240,b=241] + a=255 java.awt.Color[r=255,g=0,b=255] + a=17
    #f0f10 java.awt.Color[r=15,g=15,b=16] + a=255 null
    #f0f109 java.awt.Color[r=240,g=241,b=9] + a=255 java.awt.Color[r=240,g=241,b=9] + a=255 🟢
    #f0f1092 java.awt.Color[r=240,g=241,b=9] + a=255 null
    #f0f10928 java.awt.Color[r=240,g=241,b=9] + a=255 java.awt.Color[r=240,g=241,b=153] + a=40
    f0f10928 null java.awt.Color[r=240,g=241,b=9] + a=40
    #f0f109289 java.awt.Color[r=240,g=241,b=9] + a=255 null
    f0f109289 null null 🟢
    ppabcdef null null 🟢
    b52k null null 🟢
    #ppabcdef null null 🟢
    #b52k null null 🟢
    #ffffffff java.awt.Color[r=255,g=255,b=255] + a=255 java.awt.Color[r=255,g=255,b=255] + a=255 🟢
    ffffffff null java.awt.Color[r=255,g=255,b=255] + a=255
    #ffffff java.awt.Color[r=255,g=255,b=255] + a=255 java.awt.Color[r=255,g=255,b=255] + a=255 🟢
    ffffff java.awt.Color[r=255,g=255,b=255] + a=255 java.awt.Color[r=255,g=255,b=255] + a=255 🟢

Details :

  • Remind : (During the parsing process, before this method)
    • Leading and trailing white-spaces (according to Java) in the argument string were removed.
    • Inside () multiple white-space (according to Java) occurences are replaced by only one space character.
    • white-spaces (according to Java) between rgb or rgba and ( are removed.

Implementation examples

  • First proposition

    private static Pattern hex = Pattern.compile("\\p{XDigit}+");
    /**
    * Convert a "#FFF", "#FFFF", "#FFFFFF" or "#FFFFFFFF" hex string to a Color.
    * If the color specification is bad, an attempt
    * will be made to fix it up.
    */
    static final Color hexToColor(String digits) {
    int n = digits.length();
    if (digits.startsWith("#")) {
    digits = digits.substring(1, Math.min(n, 9));
    n--;
    }
    // CSS level 4
    // - defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
    // - allows, webpage passes 3, 4, 6 or 8 digit color code.
    // - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
    // - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
    // - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
    // - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
    //
    // Becareful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
    // Since this method is defined in CSS.java, it must only take in charge CSS Level 4 color format.
    //
    // According notes below the current OpenJDK implementation is
    // - 3 digits #[R][G][B] represents #[RR][GG][BB]FF
    // - 6 digits #[R][G][B] represents #[RR][GG][BB]FF
    //
    // Some webpage passes 3 digit color code as in #fff which is
    // decoded as #000FFF resulting in blue background.
    // As per https://www.w3.org/TR/CSS1/#color-units,
    // The three-digit RGB notation (#rgb) is converted into six-digit form
    // (#rrggbb) by replicating digits, not by adding zeros.
    // This makes sure that white (#ffffff) can be specified with the short notation
    // (#fff) and removes any dependencies on the color depth of the display.
    if (n == 3 && hex.matcher(digits).matches()) {
    final String r = digits.substring(0, 1);
    final String g = digits.substring(1, 2);
    final String b = digits.substring(2, 3);
    digits = String.format("%s%s%s%s%s%sff", r, r, g, g, b, b);
    } else if (n==4 && hex.matcher(digits).matches()) {
    final String r = digits.substring(0, 1);
    final String g = digits.substring(1, 2);
    final String b = digits.substring(2, 3);
    final String a = digits.substring(3, 4);
    digits = String.format("%s%s%s%s%s%s%s%s", r, r, g, g, b, b, a, a);
    } else if (n == 6 && hex.matcher(digits).matches()) {
    digits = String.format("%sff", digits);
    } else if (n != 8 || !hex.matcher(digits).matches()) {
    return null;
    }
    try {
    Integer intValue = Integer.parseUnsignedInt(digits, 16);
    int l = intValue.intValue();
    return new Color((l >> 24) & 0xFF,(l >> 16) & 0xFF, (l >> 8) & 0xFF, l & 0xFF);
    } catch (NumberFormatException nfe) {
    return null;
    }
    }

    Notes :

    • It compiles.
    • It runs and pass the test (see results below)
    • length and match pattern are separated for performance. gain 10%.
      if (n == 3 && hex.matcher(digits).matches()) {
    • As suggested by a main-line reviewer, chartAt replace substring methods.
    • As suggested by a main-line reviewer, try to improve tests with < rather than ==. In fact, try to mix his proposition in my test flow.
  • Optimized version using Map : Main-line discussions lead to this new approach.

    • Previous performance results were not very good.
    • Debate was stuck by minor performance considerations.
    • This new proposal seems to solve all difficulties.
    • Performance results are 6 to 50 times better than previous implementions .
      • The codes before this PR ran in 230ms.
      • Previous codes ran in 1 200ms to 1800 ms with String + formatted + %n$s usage.
      • They ran in 350ms to 380ms with String + formatted + %s usage.
      • And in 100ms to 110ms if we replace String + format with a string concatenation.
      • The code below with one Map gives the same results (including number format validation) in 36ms.
      • Since we are controlling notation length we
        • can bypass some controls,
        • directly generate the color value,
        • without generate a new string,
        • and reject a wrong number format without generate any exception.
        • Done : Problem with charAt out of bounds possible ! Not when using integrated parsing.
      static final Color hexToColor(String digits) {
          // CSS level 4
          // - defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
          // - allows, webpage passes 3, 4, 6 or 8 digit color code.
          //   - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
          //   - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
          //   - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
          //   - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
          //
          // Becareful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
          // As this method is declared in CSS.java, it must treat only format CSS Level 4.
          //
          // According notes below the current OpenJDK implementation is
          // - 3 digits #[R][G][B]    represents #[RR][GG][BB]FF
          // - 6 digits #[R][G][B]    represents #[RR][GG][BB]FF
          //
          // Some webpage passe 3 digit color code as in #fff which is
          // decoded as #000FFF resulting in blue background.
          // As per https://www.w3.org/TR/CSS1/#color-units,
          // The three-digit RGB notation (#rgb) is converted into six-digit form
          // (#rrggbb) by replicating digits, not by adding zeros.
          // This makes sure that white (#ffffff) can be specified with the short notation
          // (#fff) and removes any dependencies on the color depth of the display.
          final int st = digits.startsWith("#") ? 1 : 0;
          final byte[] idseq = digit.get(Integer.valueOf(digits.length() - st));
          if (idseq == null) {
              // Rejects string argument with a wrong number length.
              return null;
          }
          // Only 3, 4, 6 and 8 digits notations.
          // Parses the string argument and build color value
          final long value = parseHex(st, digits, idseq);
          return (value == -1 ? null : new Color((int)value, true));
      }
    
      private static final long parseHex(int st, String digits, byte[] idseq) {
          int dv;
          long value = 0;
          for (byte i : idseq) {
              value *= 16;
              dv = 15;
              if (i != -1) {
                  if ((dv = Character.digit(digits.charAt(st + i), 16)) < 0) {
                      // Rejects string argument with not a valid digit in the radix-16
                      return -1;
                  }
              }
              value += dv;
          }
          return value;
      }
    
      // Index of the Digit in the Sequence -1 means, let the default value.
      private static Map<Integer, byte[]> digit =
          Map.ofEntries(
              Map.entry(Integer.valueOf(3), new byte[]{-1, -1, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(4), new byte[]{3, 3, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(6), new byte[]{-1, -1, 0, 1, 2, 3, 4, 5}),
              Map.entry(Integer.valueOf(8), new byte[]{6, 7, 0, 1, 2, 3, 4, 5})
          );
    

    Notes :

    • It compiles.
    • It runs and pass the test, see results below (Custom Pars.).
      static final Color hexToColor(String digits) {
          // CSS level 4
          // - defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
          // - allows, webpage passes 3, 4, 6 or 8 digit color code.
          //   - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
          //   - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
          //   - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
          //   - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
          //
          // Becareful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
          // As this method is declared in CSS.java, it must treat only format CSS Level 4.
          //
          // According notes below the current OpenJDK implementation is
          // - 3 digits #[R][G][B]    represents #[RR][GG][BB]FF
          // - 6 digits #[R][G][B]    represents #[RR][GG][BB]FF
          //
          // Some webpage passe 3 digit color code as in #fff which is
          // decoded as #000FFF resulting in blue background.
          // As per https://www.w3.org/TR/CSS1/#color-units,
          // The three-digit RGB notation (#rgb) is converted into six-digit form
          // (#rrggbb) by replicating digits, not by adding zeros.
          // This makes sure that white (#ffffff) can be specified with the short notation
          // (#fff) and removes any dependencies on the color depth of the display.
          final int st = digits.startsWith("#") ? 1 : 0;
          final byte[] idseq = digit.get(Integer.valueOf(digits.length() - st));
          if (idseq == null) {
              // Rejects string argument with a wrong number length.
              return null;
          }
          // Only 3, 4, 6 and 8 digits notations.
          // Parses the string argument and build color value
          final long value = parseHex(st, digits, idseq);
          return (value == -1 ? null : new Color((int)value, true));
      }
    
      private static final long parseHex(int st, String digits, byte[] idseq) {
          int dv;
          long value = 0;
          for (byte i : idseq) {
              dv = 15;
              if (i != -1 && (dv = Character.digit(digits.charAt(st + i), 16)) < 0) {
                  // Rejects string argument with not a valid digit in the radix-16
                  return -1;   
              }
              value = value * 16 + dv;
          }
          return value;
      }
    
      // Index of the Digit in the Sequence -1 means, let the default value.
      private static Map<Integer, byte[]> digit =
          Map.ofEntries(
              Map.entry(Integer.valueOf(3), new byte[]{-1, -1, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(4), new byte[]{3, 3, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(6), new byte[]{-1, -1, 0, 1, 2, 3, 4, 5}),
              Map.entry(Integer.valueOf(8), new byte[]{6, 7, 0, 1, 2, 3, 4, 5})
          );
    

    Notes :

    • Simplification of the code.
      static final Color hexToColor(String digits) {
          // CSS level 4
          // - defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
          // - allows, webpage passes 3, 4, 6 or 8 digit color code.
          //   - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
          //   - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
          //   - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
          //   - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
          //
          // Becareful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
          // As this method is declared in CSS.java, it must treat only format CSS Level 4.
          //
          // According notes below the current OpenJDK implementation is
          // - 3 digits #[R][G][B]    represents #[RR][GG][BB]FF
          // - 6 digits #[R][G][B]    represents #[RR][GG][BB]FF
          //
          // Some webpage passe 3 digit color code as in #fff which is
          // decoded as #000FFF resulting in blue background.
          // As per https://www.w3.org/TR/CSS1/#color-units,
          // The three-digit RGB notation (#rgb) is converted into six-digit form
          // (#rrggbb) by replicating digits, not by adding zeros.
          // This makes sure that white (#ffffff) can be specified with the short notation
          // (#fff) and removes any dependencies on the color depth of the display.
          final int st = digits.startsWith("#") ? 1 : 0;
          final byte[] idseq = digit.get(Integer.valueOf(digits.length() - st));
          if (idseq == null) {
              // Rejects string argument with a wrong number length.
              return null;
          }
          // Only 3, 4, 6 and 8 digits notations.
          // Parses the string argument and build color value
          final long value = parseHex(st, digits, idseq);
          return (value == -1 ? null : new Color((int)value, true));
      }
    
      private static final long parseHex(int st, String digits, byte[] idseq) {
          int dv;
          long value = 0;
          for (byte i : idseq) {
              if ((dv = i) != 15 && (dv = Character.digit(digits.charAt(st + i), 16)) < 0) {
                  // Rejects string argument with not a valid digit in the radix-16
                  return -1;
              }
              value = (value << 4) + dv;
          }
          return value;
      }
    
      // Index/Value of the Digit in the Sequence -1 means, let the default value.
      private static Map<Integer, byte[]> digit =
          Map.ofEntries(
              // 0-7 Index of the digit value in the sequence.
              // 15 value of the digit.
              Map.entry(Integer.valueOf(3), new byte[]{15, 15, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(4), new byte[]{3, 3, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(6), new byte[]{15, 15, 0, 1, 2, 3, 4, 5}),
              Map.entry(Integer.valueOf(8), new byte[]{6, 7, 0, 1, 2, 3, 4, 5})
          );
    

    Notes :

    • Replaces -1 by 15.
    • And * 16 by << 4.
      static final Color hexToColor(String digits) {
          // CSS level 4
          // - defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
          // - allows, webpage passes 3, 4, 6 or 8 digit color code.
          //   - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
          //   - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
          //   - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
          //   - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
          //
          // Becareful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
          // As this method is declared in CSS.java, it must treat only format CSS Level 4.
          //
          // According notes below the current OpenJDK implementation is
          // - 3 digits #[R][G][B]    represents #[RR][GG][BB]FF
          // - 6 digits #[R][G][B]    represents #[RR][GG][BB]FF
          //
          // Some webpage passe 3 digit color code as in #fff which is
          // decoded as #000FFF resulting in blue background.
          // As per https://www.w3.org/TR/CSS1/#color-units,
          // The three-digit RGB notation (#rgb) is converted into six-digit form
          // (#rrggbb) by replicating digits, not by adding zeros.
          // This makes sure that white (#ffffff) can be specified with the short notation
          // (#fff) and removes any dependencies on the color depth of the display.
          final byte st = digits.startsWith("#") ? (byte)1 : (byte)0;
          final byte[] idseq = digit.get(Integer.valueOf(digits.length() - st));
          if (idseq == null) {
              // Rejects string argument with a wrong number length.
              return null;
          }
          // Only 3, 4, 6 and 8 digits notations.
          // Parses the string argument and build color value
          final int value = parseHex(st, digits, idseq);
          return (value == -2 ? null : new Color(value, true));
      }
    
      private static final int parseHex(byte st, String digits, byte[] idseq) {
          int dv;
          int value = 0;
          for (byte i : idseq) {
              if ((dv = i) != 15 && (dv = Character.digit(digits.charAt(i + st), 16)) < 0) {
                  // Rejects string argument with not a valid digit in the radix-16
                  return -2;
              }
              value = dv | value << 4;
          }
          return value;
      }
    
      // Index of the Digit Value or simply the Digit Value.
      private static Map<Integer, byte[]> digit =
          Map.ofEntries(
              // From 0 to 7, index of the digit value in the sequence.
              // 15, the default digit value.
              Map.entry(Integer.valueOf(3), new byte[]{15, 15, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(4), new byte[]{3, 3, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(6), new byte[]{15, 15, 0, 1, 2, 3, 4, 5}),
              Map.entry(Integer.valueOf(8), new byte[]{6, 7, 0, 1, 2, 3, 4, 5})
          );
    

    Notes :

    • Replaces long by int.
    • Replaces + by |.
      static final Color hexToColor(String digits) {
          // CSS level 4
          // - defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
          // - allows, webpage passes 3, 4, 6 or 8 digit color code.
          //   - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
          //   - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
          //   - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
          //   - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
          //
          // Becareful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
          // As this method is declared in CSS.java, it must treat only format CSS Level 4.
          //
          // According notes below the current OpenJDK implementation is
          // - 3 digits #[R][G][B]    represents #[RR][GG][BB]FF
          // - 6 digits #[R][G][B]    represents #[RR][GG][BB]FF
          //
          // Some webpage passe 3 digit color code as in #fff which is
          // decoded as #000FFF resulting in blue background.
          // As per https://www.w3.org/TR/CSS1/#color-units,
          // The three-digit RGB notation (#rgb) is converted into six-digit form
          // (#rrggbb) by replicating digits, not by adding zeros.
          // This makes sure that white (#ffffff) can be specified with the short notation
          // (#fff) and removes any dependencies on the color depth of the display.
          final byte st = digits.startsWith("#") ? (byte)1 : (byte)0;
          final byte[] idseq = digit.get(Integer.valueOf(digits.length() - st));
          if (idseq == null) {
              // Rejects string argument with a wrong number length.
              return null;
          }
          // Only 3, 4, 6 and 8 digits notations.
          // Parses the string argument and build color value.
          int dv;
          int value = 0;
          for (byte i : idseq) {
              if ((dv = i) != 15 && (dv = Character.digit(digits.charAt(i + st), 16)) < 0) {
                  // Rejects string argument with not a valid digit in the radix-16
                  return null;
              }
              value = dv | value << 4;
          }
          return new Color(value, true);
      }
    
      // Index of the Digit Value or simply the Digit Value.
      private static Map<Integer, byte[]> digit =
          Map.ofEntries(
              // From 0 to 7, index of the digit value in the sequence.
              // 15, the default digit value.
              Map.entry(Integer.valueOf(3), new byte[]{15, 15, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(4), new byte[]{3, 3, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(6), new byte[]{15, 15, 0, 1, 2, 3, 4, 5}),
              Map.entry(Integer.valueOf(8), new byte[]{6, 7, 0, 1, 2, 3, 4, 5})
          );
    

    Notes :

    • Back to an integrated parsing process rather than using a parsing method.
      static final Color hexToColor(String digits) {
          // CSS level 4
          // - defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
          // - allows, webpage passes 3, 4, 6 or 8 digit color code.
          //   - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
          //   - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
          //   - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
          //   - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
          //
          // Becareful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
          // As this method is declared in CSS.java, it must treat only format CSS Level 4.
          //
          // According notes below the current OpenJDK implementation is
          // - 3 digits #[R][G][B]    represents #[RR][GG][BB]FF
          // - 6 digits #[R][G][B]    represents #[RR][GG][BB]FF
          //
          // Some webpage passes 3 digit color code as in #fff which is
          // decoded as #000FFF resulting in blue background.
          // As per https://www.w3.org/TR/CSS1/#color-units,
          // The three-digit RGB notation (#rgb) is converted into six-digit form
          // (#rrggbb) by replicating digits, not by adding zeros.
          // This makes sure that white (#ffffff) can be specified with the short notation
          // (#fff) and removes any dependencies on the color depth of the display.
          final byte[] idseq = digits.startsWith("#") ?
                                   digitl.get(Long.valueOf(digits.length())):
                                   digitl.get(Long.valueOf(-digits.length()));
          if (idseq == null) {
              // Rejects string argument with a wrong number length.
              return null;
          }
          // Only 3, 4, 6 and 8 digits notations.
          // Parses the string argument and build color value.
          int dv;
          int value = 0;
          for (byte i : idseq) {
              if ((dv = i) != 15 && (dv = Character.digit(digits.charAt(i), 16)) < 0) {
                  // Rejects string argument with not a valid digit in the radix-16.
                  return null;
              }
              value = dv | value << 4;
          }
          return new Color(value, true);
      }
    
      // Index of the Digit Value or simply the Digit Value. # not prefixed.
      private static Map<Long, byte[]> digitl =
          Map.ofEntries(
              // From 1 to 8, index of the digit value in the sequence, when # prefixed
              // From 0 to 7, index of the digit value in the sequence, when # not prefixed
              // 15, the default digit value.
              Map.entry(Long.valueOf(4), new byte[]{15, 15, 1, 1, 2, 2, 3, 3}),
              Map.entry(Long.valueOf(5), new byte[]{4, 4, 1, 1, 2, 2, 3, 3}),
              Map.entry(Long.valueOf(7), new byte[]{15, 15, 1, 2, 3, 4, 5, 6}),
              Map.entry(Long.valueOf(9), new byte[]{7, 8, 1, 2, 3, 4, 6, 6}),
              Map.entry(Long.valueOf(-3), new byte[]{15, 15, 0, 0, 1, 1, 2, 2}),
              Map.entry(Long.valueOf(-4), new byte[]{3, 3, 0, 0, 1, 1, 2, 2}),
              Map.entry(Long.valueOf(-6), new byte[]{15, 15, 0, 1, 2, 3, 4, 5}),
              Map.entry(Long.valueOf(-8), new byte[]{6, 7, 0, 1, 2, 3, 4, 5})
          );
    

    Notes :

    • Maps the two case prefixed and not prefixed.
    • Becareful some Long operations are not atomic. Then retry to use string. First times, performances seemed no good, test again !
      /**
       * Convert a "#FFF", "#FFFF", "#FFFFFF" or "#FFFFFFFF" hex string to a Color.
       * If the color specification is bad, an attempt will be made to fix it up.
       */
      static final Color hexToColor(String digits) {
          // CSS Color level 4 allows webpage passes 3, 4, 6 or 8 digit color codes.
          //   - 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
          //   - 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
          //   - 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
          //   - 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]
          final byte[] iseq = digits.startsWith("#") ?
                                   iseqmap.get(Integer.valueOf(digits.length())):
                                   iseqmap.get(Integer.valueOf(-digits.length()));
          if (iseq == null) {
              // Rejects string argument with a wrong number length.
              return null;
          }
          // Only 3, 4, 6 and 8 digits notations.
          // Parses the string argument and build color value.
          int dv;
          int value = 0;
          for (byte i : iseq) {
              if ((dv = -i) != 15 && (dv = Character.digit(digits.charAt(i), 16)) < 0) {
                  // Rejects string argument with not a valid digit in the radix-16.
                  return null;
              }
              value = dv | value << 4;
          }
          return new Color(value, true);
      }
    
      // Map of Index Sequences. Index -15 means, use the default value 15.
      private static final Map<Integer, byte[]> iseqmap =
          Map.ofEntries(
              // Positive key, for # prefixed string, is associated with index from 1 to 8.
              // Negative key, for not # prefixed string, is associated with index from 0 to 7.
              Map.entry(Integer.valueOf(4), new byte[]{-15, -15, 1, 1, 2, 2, 3, 3}),
              Map.entry(Integer.valueOf(5), new byte[]{4, 4, 1, 1, 2, 2, 3, 3}),
              Map.entry(Integer.valueOf(7), new byte[]{-15, -15, 1, 2, 3, 4, 5, 6}),
              Map.entry(Integer.valueOf(9), new byte[]{7, 8, 1, 2, 3, 4, 6, 6}),
              Map.entry(Integer.valueOf(-3), new byte[]{-15, -15, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(-4), new byte[]{3, 3, 0, 0, 1, 1, 2, 2}),
              Map.entry(Integer.valueOf(-6), new byte[]{-15, -15, 0, 1, 2, 3, 4, 5}),
              Map.entry(Integer.valueOf(-8), new byte[]{6, 7, 0, 1, 2, 3, 4, 5})
          );
    

    Notes :

    • Maps the two case prefixed and not prefixed.
    • Back to Integer.
    • This method is too strict ! Test with "#1fe56980 ", "#1fe56980 ". Made changes to be able to parse this kind of string. Unnecessary, all leading and trailing spaces have been removed during the parsing process.
    • Should I declare idseq as final for the same reasons a, b, c were declared final in the previous version.

The code to test the present implementation.

  • Done : This is not a regression test. The regression test has been published.
  • Done : Test all values red, green, blue and alpha, but only for this repository, main line stream preferred a more simple one.
import java.awt.Color;
import javax.swing.text.html.StyleSheet;

public class Hex3468DigitsColorInCSS {

    public static void main(String[] args) {
        StringBuilder result = new StringBuilder();
        boolean passed = true;
        int red, green, blue, alpha;
        Color color;
        StyleSheet styleSheet = new StyleSheet();

        // #rgba Should be interpreted as #rrggbbaa according CSS Color Level 4.
        // Then expecting r=255 g=17 b=34 a=170
        color = styleSheet.stringToColor("#f12a");
        alpha = color.getAlpha();
        result.append("  Result for #f00a -> expected r=255 g=17 b=34 a=170 -> %s Alpha : %s".formatted(color.toString(), alpha));
        if (alpha != 170) {
           passed = false;
        }
        // In #rrggbbaa last two digits should be interpreted as Alpha value according CSS Color Level 4.
        // Then expecting r=255 g=17 b=34 a=170
        color = styleSheet.stringToColor("#ff1122aa");
        alpha = color.getAlpha();
        result.append("  Result for #ff1122aa -> expected r=255 g=17 b=34 a=170 -> %s Alpha : %s\n".formatted(color.toString(), alpha));
        if (alpha != 170) {
           passed = false;
        }
        result.insert(0, passed ? "Succeed " : "Failed :");
        System.out.println(result);
    }
}

Implementation evaluations : according the number of tests to reach a case

Nb of digits Original Code Binary tree Sequential + duplicate Duplicate only
0 4 2 1 5
1 4 2 1 5
2 4 2 1 5
3 1 3 3 2
4 2 3 3 3
5 4 3 2 5
6 3 3 3 3
7 4 3 3 5
8 4 3 4 5
9 4 3 4 5
... 4 3 4 5

Test results :

[scientificwaredev@localhost openjdk-compile-tests]$ ./ce_jdk_unpatched_test_Hex3468DigitsColorInCSS
Failed :  Result for #f00a -> expected r=255 g=17 b=34 a=170 -> java.awt.Color[r=0,g=241,b=42] Alpha : 255  Result for #ff1122aa -> expected r=255 g=17 b=34 a=170 -> java.awt.Color[r=255,g=17,b=34] Alpha : 255
[scientificwaredev@localhost openjdk-compile-tests]$ ./ce_jdk_patched_test_Hex3468DigitsColorInCSS
Succeed   Result for #f00a -> expected r=255 g=17 b=34 a=170 -> java.awt.Color[r=255,g=17,b=34] Alpha : 170  Result for #ff1122aa -> expected r=255 g=17 b=34 a=170 -> java.awt.Color[r=255,g=17,b=34] Alpha : 170

Regression test

public class Hex3468DigitsColor {
public static void main(String[] args) {
StringBuilder result = new StringBuilder();
boolean passed = true;
StyleSheet styleSheet = new StyleSheet();
// #rgba Should be interpreted as #rrggbbaa according CSS Color Level 4.
// Then expecting r=255 g=17 b=34 a=170
Color color = styleSheet.stringToColor("#f12a");
int red = color.getRed();
int green = color.getGreen();
int blue = color.getBlue();
int alpha = color.getAlpha();
result.append(" Test for #f00a");
if (red != 255) {
result.append(", expected r=255 but r=%s found".formatted(red));
passed = false;
}
if (green != 17) {
result.append(", expected g=17 but g=%s found".formatted(green));
passed = false;
}
if (blue != 34) {
result.append(", expected b=34 but b=%s found".formatted(blue));
passed = false;
}
if (alpha != 170) {
result.append(", expected a=170 but a=%s found".formatted(alpha));
passed = false;
}
// In #rrggbbaa last two digits should be interpreted as Alpha value according CSS Color Level 4.
// Then expecting r=255 g=17 b=34 a=170
color = styleSheet.stringToColor("#ff1122aa");
alpha = color.getAlpha();
result.append("\n Test for #ff1122aa");
if (red != 255) {
result.append(", expected r=255 but r=%s found".formatted(red));
passed = false;
}
if (green != 17) {
result.append(", expected g=17 but g=%s found".formatted(green));
passed = false;
}
if (blue != 34) {
result.append(", expected b=34 but b=%s found".formatted(blue));
passed = false;
}
if (alpha != 170) {
result.append(", expected a=170 but a=%s found".formatted(alpha));
passed = false;
}
if (!passed) {
result.insert(0, "Failed :");
throw new RuntimeException(result.toString());
}
}
}

[scientificwaredev@localhost openjdk-compile-tests]$ ./ce_jdk_unpatched_test_Hex3468DigitsColor
Exception in thread "main" java.lang.RuntimeException: Failed :  Test for #f00a, expected r=255 but r=0 found, expected g=17 but g=241 found, expected b=34 but b=42 found, expected a=170 but a=255 found
  Test for #ff1122aa, expected r=255 but r=0 found, expected g=17 but g=241 found, expected b=34 but b=42 found, expected a=170 but a=255 found
        at Hex3468DigitsColor.main(Hex3468DigitsColor.java:90)
[scientificwaredev@localhost openjdk-compile-tests]$ ./ce_jdk_patched_test_Hex3468DigitsColor

About performances

  • Enhancements must be at least without performance cost.
  • What is tested :
    • 1 000 occurences of :
      • a set of 1 600 valid color notations, equi-distributed (Not Real WWW).
      • and 1 wrong color notation.
Run Nb Tests First Imp. subString (ms) Sugg Imp. char %n$s (ms) Sugg. Imp. char %n$s dis. mis comb. 3 & 4 (ms) Bin. Tree (ms) Bin. Tree (ms) Bin. Tree (ms) Bin. Tree (ms) dis. mis comb. 3 & 4 (ms) Custom Pars. 1 Map (ms) Before PR (ms)
format %s ✔️ ✔️
format %n$s ✔️ ✔️ ✔️
""+r+... ✔️ ✔️
bit rotation ✔️
hex Pattern ✔️ ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
🥉 🥈 🥇
1 1 601 000 351 1 790 1 825 1 860 374 178 180 110 36 229
2 1 601 000 346 1 253 1 297 1 913 385 177 180 110 35 233
3 1 601 000 365 1 783 1 763 1 887 369 179 185 108 36 228
4 1 601 000 346 1 824 1 698 1 820 375 178 184 109 36 230
5 1 601 000 364 1 767 1 823 1 958 375 180 178 111 37 229

CSS Level 4 details

  • defines color hex code as #[2 digits Red][2 digits Green][2 digits Blue][2 digits Alpha]. With digit 0 ... f.
  • allows, webpage passes 3, 4, 6 or 8 digit color code.
    • 3 digits #[R][G][B] ........ represents #[RR][GG][BB]FF
    • 4 digits #[R][G][B][A] ..... represents #[RR][GG][BB][AA]
    • 6 digits #[RR][GG][BB] ..... represents #[RR][GG][BB]FF
    • 8 digits #[RR][GG][BB][AA] . represents #[RR][GG][BB][AA]

Becareful ! In java.awt.Color hex #[2 digits Alpha][2 digits Red][2 digits Green][2 digits Blue]
Since this method is defined in CSS.java, it must only take in charge CSS Level 4 color format.

As explained in the code, the current OpenJDK implementation follows CSS1 Recommandation

  • 3 digits #[R][G][B] represents #[RR][GG][BB]FF
  • 6 digits #[R][G][B] represents #[RR][GG][BB]FF

JDK-4895924 : Strings in format #rgb not handled by Color.decode() (affects CSS / Swing)
JDK-4895924 : Strings in format #rgb not handled by Color.decode() (affects CSS / Swing)


Id. Title Java Bug Data Base OpenJDK Bug System Status
JDK-4895924 Strings in format #rgb not handled by Color.decode() (affects CSS / Swing) Report Details 🔐
JDK-8149631 rgb(...) CSS color values are not parsed properly Report Details 🔐
@scientificware scientificware changed the title Adds CSS 4 and 8 digits hex coded Color Internal Review ID : 9073933 Adds CSS 4 and 8 digits hex coded Color Sep 10, 2022
@scientificware scientificware changed the title Internal Review ID : 9073933 Adds CSS 4 and 8 digits hex coded Color Int. Rev. ID : 9073933 Adds CSS 4 and 8 digits hex coded Color Sep 10, 2022
@scientificware scientificware self-assigned this Sep 10, 2022
@scientificware scientificware added Reporter Reported this Issue to Java Bug DataBase Author Contributed a patch to fix this issue Enhancement New feature or request WIP Work In Progress labels Sep 10, 2022
@scientificware scientificware changed the title Int. Rev. ID : 9073933 Adds CSS 4 and 8 digits hex coded Color JDK-8293776 : Adds CSS 4 and 8 digits hex coded Color Sep 14, 2022
scientificware added a commit that referenced this issue Sep 17, 2022
Adds a regression for DK-8293776 : Adds CSS 4 and 8 digits hex coded Color #13
@scientificware scientificware added the Fixed Fixed issue. label Sep 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author Contributed a patch to fix this issue Enhancement New feature or request Fixed Fixed issue. Reporter Reported this Issue to Java Bug DataBase WIP Work In Progress
Projects
None yet
Development

When branches are created from issues, their pull requests are automatically linked.

1 participant