1 /*
   2  * Copyright (c) 2003, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 /*
  27  *
  28  * (C) Copyright IBM Corp. 1999-2003 - All Rights Reserved
  29  *
  30  * The original version of this source code and documentation is
  31  * copyrighted and owned by IBM. These materials are provided
  32  * under terms of a License Agreement between IBM and Sun.
  33  * This technology is protected by multiple US and International
  34  * patents. This notice and attribution to IBM may not be removed.
  35  */
  36 
  37 /*
  38  * GlyphLayout is used to process a run of text into a run of run of
  39  * glyphs, optionally with position and char mapping info.
  40  *
  41  * The text has already been processed for numeric shaping and bidi.
  42  * The run of text that layout works on has a single bidi level.  It
  43  * also has a single font/style.  Some operations need context to work
  44  * on (shaping, script resolution) so context for the text run text is
  45  * provided.  It is assumed that the text array contains sufficient
  46  * context, and the offset and count delimit the portion of the text
  47  * that needs to actually be processed.
  48  *
  49  * The font might be a composite font.  Layout generally requires
  50  * tables from a single physical font to operate, and so it must
  51  * resolve the 'single' font run into runs of physical fonts.
  52  *
  53  * Some characters are supported by several fonts of a composite, and
  54  * in order to properly emulate the glyph substitution behavior of a
  55  * single physical font, these characters might need to be mapped to
  56  * different physical fonts.  The script code that is assigned
  57  * characters normally considered 'common script' can be used to
  58  * resolve which physical font to use for these characters. The input
  59  * to the char to glyph mapper (which assigns physical fonts as it
  60  * processes the glyphs) should include the script code, and the
  61  * mapper should operate on runs of a single script.
  62  *
  63  * To perform layout, call get() to get a new (or reuse an old)
  64  * GlyphLayout, call layout on it, then call done(GlyphLayout) when
  65  * finished.  There's no particular problem if you don't call done,
  66  * but it assists in reuse of the GlyphLayout.
  67  */
  68 
  69 package sun.font;
  70 
  71 import java.lang.ref.SoftReference;
  72 import java.awt.Font;
  73 import java.awt.font.FontRenderContext;
  74 import java.awt.font.GlyphVector;
  75 import java.awt.geom.AffineTransform;
  76 import java.awt.geom.NoninvertibleTransformException;
  77 import java.awt.geom.Point2D;
  78 import java.util.ArrayList;
  79 import java.util.concurrent.ConcurrentHashMap;
  80 
  81 import static java.lang.Character.*;
  82 
  83 public final class GlyphLayout {
  84     // data for glyph vector
  85     private GVData _gvdata;
  86 
  87     // cached glyph layout data for reuse
  88     private static volatile GlyphLayout cache;  // reusable
  89 
  90     private LayoutEngineFactory _lef;  // set when get is called, unset when done is called
  91     private TextRecord _textRecord;    // the text we're working on, used by iterators
  92     private ScriptRun _scriptRuns;     // iterator over script runs
  93     private FontRunIterator _fontRuns; // iterator over physical fonts in a composite
  94     private int _ercount;
  95     private ArrayList<EngineRecord> _erecords;
  96     private Point2D.Float _pt;
  97     private FontStrikeDesc _sd;
  98     private float[] _mat;
  99     private float ptSize;
 100     private int _typo_flags;
 101     private int _offset;
 102 
 103     public static final class LayoutEngineKey {
 104         private Font2D font;
 105         private int script;
 106         private int lang;
 107 
 108         LayoutEngineKey() {
 109         }
 110 
 111         LayoutEngineKey(Font2D font, int script, int lang) {
 112             init(font, script, lang);
 113         }
 114 
 115         void init(Font2D font, int script, int lang) {
 116             this.font = font;
 117             this.script = script;
 118             this.lang = lang;
 119         }
 120 
 121         LayoutEngineKey copy() {
 122             return new LayoutEngineKey(font, script, lang);
 123         }
 124 
 125         Font2D font() {
 126             return font;
 127         }
 128 
 129         int script() {
 130             return script;
 131         }
 132 
 133         int lang() {
 134             return lang;
 135         }
 136 
 137         public boolean equals(Object rhs) {
 138             if (this == rhs) return true;
 139             if (rhs == null) return false;
 140             try {
 141                 LayoutEngineKey that = (LayoutEngineKey)rhs;
 142                 return this.script == that.script &&
 143                        this.lang == that.lang &&
 144                        this.font.equals(that.font);
 145             }
 146             catch (ClassCastException e) {
 147                 return false;
 148             }
 149         }
 150 
 151         public int hashCode() {
 152             return script ^ lang ^ font.hashCode();
 153         }
 154     }
 155 
 156     public static interface LayoutEngineFactory {
 157         /**
 158          * Given a font, script, and language, determine a layout engine to use.
 159          */
 160         public LayoutEngine getEngine(Font2D font, int script, int lang);
 161 
 162         /**
 163          * Given a key, determine a layout engine to use.
 164          */
 165         public LayoutEngine getEngine(LayoutEngineKey key);
 166     }
 167 
 168     public static interface LayoutEngine {
 169         /**
 170          * Given a strike descriptor, text, rtl flag, and starting point, append information about
 171          * glyphs, positions, and character indices to the glyphvector data, and advance the point.
 172          *
 173          * If the GVData does not have room for the glyphs, throws an IndexOutOfBoundsException and
 174          * leave pt and the gvdata unchanged.
 175          */
 176         public void layout(FontStrikeDesc sd, float[] mat, float ptSize, int gmask,
 177                            int baseIndex, TextRecord text, int typo_flags, Point2D.Float pt, GVData data);
 178     }
 179 
 180     /**
 181      * Return a new instance of GlyphLayout, using the provided layout engine factory.
 182      * If null, the system layout engine factory will be used.
 183      */
 184     public static GlyphLayout get(LayoutEngineFactory lef) {
 185         if (lef == null) {
 186             lef = SunLayoutEngine.instance();
 187         }
 188         GlyphLayout result = null;
 189         synchronized(GlyphLayout.class) {
 190             if (cache != null) {
 191                 result = cache;
 192                 cache = null;
 193             }
 194         }
 195         if (result == null) {
 196             result = new GlyphLayout();
 197         }
 198         result._lef = lef;
 199         return result;
 200     }
 201 
 202     /**
 203      * Return the old instance of GlyphLayout when you are done.  This enables reuse
 204      * of GlyphLayout objects.
 205      */
 206     public static void done(GlyphLayout gl) {
 207         gl._lef = null;
 208         cache = gl; // object reference assignment is thread safe, it says here...
 209     }
 210 
 211     private static final class SDCache {
 212         public Font key_font;
 213         public FontRenderContext key_frc;
 214 
 215         public AffineTransform dtx;
 216         public AffineTransform gtx;
 217         public Point2D.Float delta;
 218         public FontStrikeDesc sd;
 219 
 220         private SDCache(Font font, FontRenderContext frc) {
 221             key_font = font;
 222             key_frc = frc;
 223 
 224             // !!! add getVectorTransform and hasVectorTransform to frc?  then
 225             // we could just skip this work...
 226 
 227             dtx = frc.getTransform();
 228             dtx.setTransform(dtx.getScaleX(), dtx.getShearY(),
 229                              dtx.getShearX(), dtx.getScaleY(),
 230                              0, 0);
 231 
 232             float ptSize = font.getSize2D();
 233             if (font.isTransformed()) {
 234                 gtx = font.getTransform();
 235                 gtx.scale(ptSize, ptSize);
 236                 delta = new Point2D.Float((float)gtx.getTranslateX(),
 237                                           (float)gtx.getTranslateY());
 238                 gtx.setTransform(gtx.getScaleX(), gtx.getShearY(),
 239                                  gtx.getShearX(), gtx.getScaleY(),
 240                                  0, 0);
 241                 gtx.preConcatenate(dtx);
 242             } else {
 243                 delta = ZERO_DELTA;
 244                 gtx = new AffineTransform(dtx);
 245                 gtx.scale(ptSize, ptSize);
 246             }
 247 
 248             /* Similar logic to that used in SunGraphics2D.checkFontInfo().
 249              * Whether a grey (AA) strike is needed is size dependent if
 250              * AA mode is 'gasp'.
 251              */
 252             int aa =
 253                 FontStrikeDesc.getAAHintIntVal(frc.getAntiAliasingHint(),
 254                                                FontUtilities.getFont2D(font),
 255                                                (int)Math.abs(ptSize));
 256             int fm = FontStrikeDesc.getFMHintIntVal
 257                 (frc.getFractionalMetricsHint());
 258             sd = new FontStrikeDesc(dtx, gtx, font.getStyle(), aa, fm);
 259         }
 260 
 261         private static final Point2D.Float ZERO_DELTA = new Point2D.Float();
 262 
 263         private static
 264             SoftReference<ConcurrentHashMap<SDKey, SDCache>> cacheRef;
 265 
 266         private static final class SDKey {
 267             private final Font font;
 268             private final FontRenderContext frc;
 269             private final int hash;
 270 
 271             SDKey(Font font, FontRenderContext frc) {
 272                 this.font = font;
 273                 this.frc = frc;
 274                 this.hash = font.hashCode() ^ frc.hashCode();
 275             }
 276 
 277             public int hashCode() {
 278                 return hash;
 279             }
 280 
 281             public boolean equals(Object o) {
 282                 try {
 283                     SDKey rhs = (SDKey)o;
 284                     return
 285                         hash == rhs.hash &&
 286                         font.equals(rhs.font) &&
 287                         frc.equals(rhs.frc);
 288                 }
 289                 catch (ClassCastException e) {
 290                 }
 291                 return false;
 292             }
 293         }
 294 
 295         public static SDCache get(Font font, FontRenderContext frc) {
 296 
 297             // It is possible a translation component will be in the FRC.
 298             // It doesn't affect us except adversely as we would consider
 299             // FRC's which are really the same to be different. If we
 300             // detect a translation component, then we need to exclude it
 301             // by creating a new transform which excludes the translation.
 302             if (frc.isTransformed()) {
 303                 AffineTransform transform = frc.getTransform();
 304                 if (transform.getTranslateX() != 0 ||
 305                     transform.getTranslateY() != 0) {
 306                     transform = new AffineTransform(transform.getScaleX(),
 307                                                     transform.getShearY(),
 308                                                     transform.getShearX(),
 309                                                     transform.getScaleY(),
 310                                                     0, 0);
 311                     frc = new FontRenderContext(transform,
 312                                                 frc.getAntiAliasingHint(),
 313                                                 frc.getFractionalMetricsHint()
 314                                                 );
 315                 }
 316             }
 317 
 318             SDKey key = new SDKey(font, frc); // garbage, yuck...
 319             ConcurrentHashMap<SDKey, SDCache> cache = null;
 320             SDCache res = null;
 321             if (cacheRef != null) {
 322                 cache = cacheRef.get();
 323                 if (cache != null) {
 324                     res = cache.get(key);
 325                 }
 326             }
 327             if (res == null) {
 328                 res = new SDCache(font, frc);
 329                 if (cache == null) {
 330                     cache = new ConcurrentHashMap<SDKey, SDCache>(10);
 331                     cacheRef = new
 332                        SoftReference<ConcurrentHashMap<SDKey, SDCache>>(cache);
 333                 } else if (cache.size() >= 512) {
 334                     cache.clear();
 335                 }
 336                 cache.put(key, res);
 337             }
 338             return res;
 339         }
 340     }
 341 
 342     /**
 343      * Create a glyph vector.
 344      * @param font the font to use
 345      * @param frc the font render context
 346      * @param text the text, including optional context before start and after start + count
 347      * @param offset the start of the text to lay out
 348      * @param count the length of the text to lay out
 349      * @param flags bidi and context flags {@see #java.awt.Font}
 350      * @param result a StandardGlyphVector to modify, can be null
 351      * @return the layed out glyphvector, if result was passed in, it is returned
 352      */
 353     public StandardGlyphVector layout(Font font, FontRenderContext frc,
 354                                       char[] text, int offset, int count,
 355                                       int flags, StandardGlyphVector result)
 356     {
 357         if (text == null || offset < 0 || count < 0 || (count > text.length - offset)) {
 358             throw new IllegalArgumentException();
 359         }
 360 
 361         init(count);
 362 
 363         // need to set after init
 364         // go through the back door for this
 365         if (font.hasLayoutAttributes()) {
 366             AttributeValues values = ((AttributeMap)font.getAttributes()).getValues();
 367             if (values.getKerning() != 0) _typo_flags |= 0x1;
 368             if (values.getLigatures() != 0) _typo_flags |= 0x2;
 369         }
 370 
 371         _offset = offset;
 372 
 373         // use cache now - can we use the strike cache for this?
 374 
 375         SDCache txinfo = SDCache.get(font, frc);
 376         _mat[0] = (float)txinfo.gtx.getScaleX();
 377         _mat[1] = (float)txinfo.gtx.getShearY();
 378         _mat[2] = (float)txinfo.gtx.getShearX();
 379         _mat[3] = (float)txinfo.gtx.getScaleY();
 380         _pt.setLocation(txinfo.delta);
 381         ptSize = font.getSize2D();
 382 
 383         int lim = offset + count;
 384 
 385         int min = 0;
 386         int max = text.length;
 387         if (flags != 0) {
 388             if ((flags & Font.LAYOUT_RIGHT_TO_LEFT) != 0) {
 389               _typo_flags |= 0x80000000; // RTL
 390             }
 391 
 392             if ((flags & Font.LAYOUT_NO_START_CONTEXT) != 0) {
 393                 min = offset;
 394             }
 395 
 396             if ((flags & Font.LAYOUT_NO_LIMIT_CONTEXT) != 0) {
 397                 max = lim;
 398             }
 399         }
 400 
 401         int lang = -1; // default for now
 402 
 403         Font2D font2D = FontUtilities.getFont2D(font);
 404         if (font2D instanceof FontSubstitution) {
 405             font2D = ((FontSubstitution)font2D).getCompositeFont2D();
 406         }
 407 
 408         _textRecord.init(text, offset, lim, min, max);
 409         int start = offset;
 410         if (font2D instanceof CompositeFont) {
 411             _scriptRuns.init(text, offset, count); // ??? how to handle 'common' chars
 412             _fontRuns.init((CompositeFont)font2D, text, offset, lim);
 413             while (_scriptRuns.next()) {
 414                 int limit = _scriptRuns.getScriptLimit();
 415                 int script = _scriptRuns.getScriptCode();
 416                 while (_fontRuns.next(script, limit)) {
 417                     Font2D pfont = _fontRuns.getFont();
 418                     /* layout can't deal with NativeFont instances. The
 419                      * native font is assumed to know of a suitable non-native
 420                      * substitute font. This currently works because
 421                      * its consistent with the way NativeFonts delegate
 422                      * in other cases too.
 423                      */
 424                     if (pfont instanceof NativeFont) {
 425                         pfont = ((NativeFont)pfont).getDelegateFont();
 426                     }
 427                     int gmask = _fontRuns.getGlyphMask();
 428                     int pos = _fontRuns.getPos();
 429                     nextEngineRecord(start, pos, script, lang, pfont, gmask);
 430                     start = pos;
 431                 }
 432             }
 433         } else {
 434             _scriptRuns.init(text, offset, count); // ??? don't worry about 'common' chars
 435             while (_scriptRuns.next()) {
 436                 int limit = _scriptRuns.getScriptLimit();
 437                 int script = _scriptRuns.getScriptCode();
 438                 nextEngineRecord(start, limit, script, lang, font2D, 0);
 439                 start = limit;
 440             }
 441         }
 442 
 443         int ix = 0;
 444         int stop = _ercount;
 445         int dir = 1;
 446 
 447         if (_typo_flags < 0) { // RTL
 448             ix = stop - 1;
 449             stop = -1;
 450             dir = -1;
 451         }
 452 
 453         //        _sd.init(dtx, gtx, font.getStyle(), frc.isAntiAliased(), frc.usesFractionalMetrics());
 454         _sd = txinfo.sd;
 455         for (;ix != stop; ix += dir) {
 456             EngineRecord er = _erecords.get(ix);
 457             for (;;) {
 458                 try {
 459                     er.layout();
 460                     break;
 461                 }
 462                 catch (IndexOutOfBoundsException e) {
 463                     if (_gvdata._count >=0) {
 464                         _gvdata.grow();
 465                     }
 466                 }
 467             }
 468             // Break out of the outer for loop if layout fails.
 469             if (_gvdata._count < 0) {
 470                 break;
 471             }
 472         }
 473 
 474         // If layout fails (negative glyph count) create an un-laid out GV instead.
 475         // ie default positions. This will be a lot better than the alternative of
 476         // a complete blank layout.
 477         StandardGlyphVector gv;
 478         if (_gvdata._count < 0) {
 479             gv = new StandardGlyphVector(font, text, offset, count, frc);
 480             if (FontUtilities.debugFonts()) {
 481                FontUtilities.getLogger().warning("OpenType layout failed on font: " +
 482                                                  font);
 483             }
 484         } else {
 485             gv = _gvdata.createGlyphVector(font, frc, result);
 486         }
 487         //        System.err.println("Layout returns: " + gv);
 488         return gv;
 489     }
 490 
 491     //
 492     // private methods
 493     //
 494 
 495     private GlyphLayout() {
 496         this._gvdata = new GVData();
 497         this._textRecord = new TextRecord();
 498         this._scriptRuns = new ScriptRun();
 499         this._fontRuns = new FontRunIterator();
 500         this._erecords = new ArrayList<>(10);
 501         this._pt = new Point2D.Float();
 502         this._sd = new FontStrikeDesc();
 503         this._mat = new float[4];
 504     }
 505 
 506     private void init(int capacity) {
 507         this._typo_flags = 0;
 508         this._ercount = 0;
 509         this._gvdata.init(capacity);
 510     }
 511 
 512     private void nextEngineRecord(int start, int limit, int script, int lang, Font2D font, int gmask) {
 513         EngineRecord er = null;
 514         if (_ercount == _erecords.size()) {
 515             er = new EngineRecord();
 516             _erecords.add(er);
 517         } else {
 518             er = _erecords.get(_ercount);
 519         }
 520         er.init(start, limit, font, script, lang, gmask);
 521         ++_ercount;
 522     }
 523 
 524     /**
 525      * Storage for layout to build glyph vector data, then generate a real GlyphVector
 526      */
 527     public static final class GVData {
 528         public int _count; // number of glyphs, >= number of chars
 529         public int _flags;
 530         public int[] _glyphs;
 531         public float[] _positions;
 532         public int[] _indices;
 533 
 534         private static final int UNINITIALIZED_FLAGS = -1;
 535 
 536         public void init(int size) {
 537             _count = 0;
 538             _flags = UNINITIALIZED_FLAGS;
 539 
 540             if (_glyphs == null || _glyphs.length < size) {
 541                 if (size < 20) {
 542                     size = 20;
 543                 }
 544                 _glyphs = new int[size];
 545                 _positions = new float[size * 2 + 2];
 546                 _indices = new int[size];
 547             }
 548         }
 549 
 550         public void grow() {
 551             grow(_glyphs.length / 4); // always grows because min length is 20
 552         }
 553 
 554         public void grow(int delta) {
 555             int size = _glyphs.length + delta;
 556             int[] nglyphs = new int[size];
 557             System.arraycopy(_glyphs, 0, nglyphs, 0, _count);
 558             _glyphs = nglyphs;
 559 
 560             float[] npositions = new float[size * 2 + 2];
 561             System.arraycopy(_positions, 0, npositions, 0, _count * 2 + 2);
 562             _positions = npositions;
 563 
 564             int[] nindices = new int[size];
 565             System.arraycopy(_indices, 0, nindices, 0, _count);
 566             _indices = nindices;
 567         }
 568 
 569         public StandardGlyphVector createGlyphVector(Font font, FontRenderContext frc, StandardGlyphVector result) {
 570 
 571             // !!! default initialization until we let layout engines do it
 572             if (_flags == UNINITIALIZED_FLAGS) {
 573                 _flags = 0;
 574 
 575                 if (_count > 1) { // if only 1 glyph assume LTR
 576                     boolean ltr = true;
 577                     boolean rtl = true;
 578 
 579                     int rtlix = _count; // rtl index
 580                     for (int i = 0; i < _count && (ltr || rtl); ++i) {
 581                         int cx = _indices[i];
 582 
 583                         ltr = ltr && (cx == i);
 584                         rtl = rtl && (cx == --rtlix);
 585                     }
 586 
 587                     if (rtl) _flags |= GlyphVector.FLAG_RUN_RTL;
 588                     if (!rtl && !ltr) _flags |= GlyphVector.FLAG_COMPLEX_GLYPHS;
 589                 }
 590 
 591                 // !!! layout engines need to tell us whether they performed
 592                 // position adjustments. currently they don't tell us, so
 593                 // we must assume they did
 594                 _flags |= GlyphVector.FLAG_HAS_POSITION_ADJUSTMENTS;
 595             }
 596 
 597             int[] glyphs = new int[_count];
 598             System.arraycopy(_glyphs, 0, glyphs, 0, _count);
 599 
 600             float[] positions = null;
 601             if ((_flags & GlyphVector.FLAG_HAS_POSITION_ADJUSTMENTS) != 0) {
 602                 positions = new float[_count * 2 + 2];
 603                 System.arraycopy(_positions, 0, positions, 0, positions.length);
 604             }
 605 
 606             int[] indices = null;
 607             if ((_flags & GlyphVector.FLAG_COMPLEX_GLYPHS) != 0) {
 608                 indices = new int[_count];
 609                 System.arraycopy(_indices, 0, indices, 0, _count);
 610             }
 611 
 612             if (result == null) {
 613                 result = new StandardGlyphVector(font, frc, glyphs, positions, indices, _flags);
 614             } else {
 615                 result.initGlyphVector(font, frc, glyphs, positions, indices, _flags);
 616             }
 617 
 618             return result;
 619         }
 620     }
 621 
 622     /**
 623      * Utility class to keep track of script runs, which may have to be reordered rtl when we're
 624      * finished.
 625      */
 626     private final class EngineRecord {
 627         private int start;
 628         private int limit;
 629         private int gmask;
 630         private int eflags;
 631         private LayoutEngineKey key;
 632         private LayoutEngine engine;
 633 
 634         EngineRecord() {
 635             key = new LayoutEngineKey();
 636         }
 637 
 638         void init(int start, int limit, Font2D font, int script, int lang, int gmask) {
 639             this.start = start;
 640             this.limit = limit;
 641             this.gmask = gmask;
 642             this.key.init(font, script, lang);
 643             this.eflags = 0;
 644 
 645             // only request canonical substitution if we have combining marks
 646             for (int i = start; i < limit; ++i) {
 647                 int ch = _textRecord.text[i];
 648                 if (isHighSurrogate((char)ch) &&
 649                     i < limit - 1 &&
 650                     isLowSurrogate(_textRecord.text[i+1])) {
 651                     // rare case
 652                     ch = toCodePoint((char)ch,_textRecord.text[++i]); // inc
 653                 }
 654                 int gc = getType(ch);
 655                 if (gc == NON_SPACING_MARK ||
 656                     gc == ENCLOSING_MARK ||
 657                     gc == COMBINING_SPACING_MARK) { // could do range test also
 658 
 659                     this.eflags = 0x4;
 660                     break;
 661                 }
 662             }
 663 
 664             this.engine = _lef.getEngine(key); // flags?
 665         }
 666 
 667         void layout() {
 668             _textRecord.start = start;
 669             _textRecord.limit = limit;
 670             engine.layout(_sd, _mat, ptSize, gmask, start - _offset, _textRecord,
 671                           _typo_flags | eflags, _pt, _gvdata);
 672         }
 673     }
 674 }