OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 // We use an underscore to avoid confusion with the standard math.h library. | |
6 #include "math_.h" | |
7 | |
8 #include <limits> | |
9 #include <vector> | |
10 | |
11 #include "layout.h" | |
12 #include "maxp.h" | |
13 | |
14 // MATH - The MATH Table | |
15 // The specification is not yet public but has been submitted to the MPEG group | |
16 // in response to the 'Call for Proposals for ISO/IEC 14496-22 "Open Font | |
17 // Format" Color Font Technology and MATH layout support'. Meanwhile, you can | |
18 // contact Microsoft's engineer Murray Sargent to obtain a copy. | |
19 | |
20 #define TABLE_NAME "MATH" | |
21 | |
22 namespace { | |
23 | |
24 // The size of MATH header. | |
25 // Version | |
26 // MathConstants | |
27 // MathGlyphInfo | |
28 // MathVariants | |
29 const unsigned kMathHeaderSize = 4 + 3 * 2; | |
30 | |
31 // The size of the MathGlyphInfo header. | |
32 // MathItalicsCorrectionInfo | |
33 // MathTopAccentAttachment | |
34 // ExtendedShapeCoverage | |
35 // MathKernInfo | |
36 const unsigned kMathGlyphInfoHeaderSize = 4 * 2; | |
37 | |
38 // The size of the MathValueRecord. | |
39 // Value | |
40 // DeviceTable | |
41 const unsigned kMathValueRecordSize = 2 * 2; | |
42 | |
43 // The size of the GlyphPartRecord. | |
44 // glyph | |
45 // StartConnectorLength | |
46 // EndConnectorLength | |
47 // FullAdvance | |
48 // PartFlags | |
49 const unsigned kGlyphPartRecordSize = 5 * 2; | |
50 | |
51 // Shared Table: MathValueRecord | |
52 | |
53 bool ParseMathValueRecord(const ots::OpenTypeFile *file, | |
54 ots::Buffer* subtable, const uint8_t *data, | |
55 const size_t length) { | |
56 // Check the Value field. | |
57 if (!subtable->Skip(2)) { | |
58 return OTS_FAILURE(); | |
59 } | |
60 | |
61 // Check the offset to device table. | |
62 uint16_t offset = 0; | |
63 if (!subtable->ReadU16(&offset)) { | |
64 return OTS_FAILURE(); | |
65 } | |
66 if (offset) { | |
67 if (offset >= length) { | |
68 return OTS_FAILURE(); | |
69 } | |
70 if (!ots::ParseDeviceTable(file, data + offset, length - offset)) { | |
71 return OTS_FAILURE(); | |
72 } | |
73 } | |
74 | |
75 return true; | |
76 } | |
77 | |
78 bool ParseMathConstantsTable(const ots::OpenTypeFile *file, | |
79 const uint8_t *data, size_t length) { | |
80 ots::Buffer subtable(data, length); | |
81 | |
82 // Part 1: int16 or uint16 constants. | |
83 // ScriptPercentScaleDown | |
84 // ScriptScriptPercentScaleDown | |
85 // DelimitedSubFormulaMinHeight | |
86 // DisplayOperatorMinHeight | |
87 if (!subtable.Skip(4 * 2)) { | |
88 return OTS_FAILURE(); | |
89 } | |
90 | |
91 // Part 2: MathValueRecord constants. | |
92 // MathLeading | |
93 // AxisHeight | |
94 // AccentBaseHeight | |
95 // FlattenedAccentBaseHeight | |
96 // SubscriptShiftDown | |
97 // SubscriptTopMax | |
98 // SubscriptBaselineDropMin | |
99 // SuperscriptShiftUp | |
100 // SuperscriptShiftUpCramped | |
101 // SuperscriptBottomMin | |
102 // | |
103 // SuperscriptBaselineDropMax | |
104 // SubSuperscriptGapMin | |
105 // SuperscriptBottomMaxWithSubscript | |
106 // SpaceAfterScript | |
107 // UpperLimitGapMin | |
108 // UpperLimitBaselineRiseMin | |
109 // LowerLimitGapMin | |
110 // LowerLimitBaselineDropMin | |
111 // StackTopShiftUp | |
112 // StackTopDisplayStyleShiftUp | |
113 // | |
114 // StackBottomShiftDown | |
115 // StackBottomDisplayStyleShiftDown | |
116 // StackGapMin | |
117 // StackDisplayStyleGapMin | |
118 // StretchStackTopShiftUp | |
119 // StretchStackBottomShiftDown | |
120 // StretchStackGapAboveMin | |
121 // StretchStackGapBelowMin | |
122 // FractionNumeratorShiftUp | |
123 // FractionNumeratorDisplayStyleShiftUp | |
124 // | |
125 // FractionDenominatorShiftDown | |
126 // FractionDenominatorDisplayStyleShiftDown | |
127 // FractionNumeratorGapMin | |
128 // FractionNumDisplayStyleGapMin | |
129 // FractionRuleThickness | |
130 // FractionDenominatorGapMin | |
131 // FractionDenomDisplayStyleGapMin | |
132 // SkewedFractionHorizontalGap | |
133 // SkewedFractionVerticalGap | |
134 // OverbarVerticalGap | |
135 // | |
136 // OverbarRuleThickness | |
137 // OverbarExtraAscender | |
138 // UnderbarVerticalGap | |
139 // UnderbarRuleThickness | |
140 // UnderbarExtraDescender | |
141 // RadicalVerticalGap | |
142 // RadicalDisplayStyleVerticalGap | |
143 // RadicalRuleThickness | |
144 // RadicalExtraAscender | |
145 // RadicalKernBeforeDegree | |
146 // | |
147 // RadicalKernAfterDegree | |
148 for (unsigned i = 0; i < static_cast<unsigned>(51); ++i) { | |
149 if (!ParseMathValueRecord(file, &subtable, data, length)) { | |
150 return OTS_FAILURE(); | |
151 } | |
152 } | |
153 | |
154 // Part 3: uint16 constant | |
155 // RadicalDegreeBottomRaisePercent | |
156 if (!subtable.Skip(2)) { | |
157 return OTS_FAILURE(); | |
158 } | |
159 | |
160 return true; | |
161 } | |
162 | |
163 bool ParseMathValueRecordSequenceForGlyphs(const ots::OpenTypeFile *file, | |
164 ots::Buffer* subtable, | |
165 const uint8_t *data, | |
166 const size_t length, | |
167 const uint16_t num_glyphs) { | |
168 // Check the header. | |
169 uint16_t offset_coverage = 0; | |
170 uint16_t sequence_count = 0; | |
171 if (!subtable->ReadU16(&offset_coverage) || | |
172 !subtable->ReadU16(&sequence_count)) { | |
173 return OTS_FAILURE(); | |
174 } | |
175 | |
176 const unsigned sequence_end = static_cast<unsigned>(2 * 2) + | |
177 sequence_count * kMathValueRecordSize; | |
178 if (sequence_end > std::numeric_limits<uint16_t>::max()) { | |
179 return OTS_FAILURE(); | |
180 } | |
181 | |
182 // Check coverage table. | |
183 if (offset_coverage < sequence_end || offset_coverage >= length) { | |
184 return OTS_FAILURE(); | |
185 } | |
186 if (!ots::ParseCoverageTable(file, data + offset_coverage, | |
187 length - offset_coverage, | |
188 num_glyphs, sequence_count)) { | |
189 return OTS_FAILURE(); | |
190 } | |
191 | |
192 // Check sequence. | |
193 for (unsigned i = 0; i < sequence_count; ++i) { | |
194 if (!ParseMathValueRecord(file, subtable, data, length)) { | |
195 return OTS_FAILURE(); | |
196 } | |
197 } | |
198 | |
199 return true; | |
200 } | |
201 | |
202 bool ParseMathItalicsCorrectionInfoTable(const ots::OpenTypeFile *file, | |
203 const uint8_t *data, | |
204 size_t length, | |
205 const uint16_t num_glyphs) { | |
206 ots::Buffer subtable(data, length); | |
207 return ParseMathValueRecordSequenceForGlyphs(file, &subtable, data, length, | |
208 num_glyphs); | |
209 } | |
210 | |
211 bool ParseMathTopAccentAttachmentTable(const ots::OpenTypeFile *file, | |
212 const uint8_t *data, | |
213 size_t length, | |
214 const uint16_t num_glyphs) { | |
215 ots::Buffer subtable(data, length); | |
216 return ParseMathValueRecordSequenceForGlyphs(file, &subtable, data, length, | |
217 num_glyphs); | |
218 } | |
219 | |
220 bool ParseMathKernTable(const ots::OpenTypeFile *file, | |
221 const uint8_t *data, size_t length) { | |
222 ots::Buffer subtable(data, length); | |
223 | |
224 // Check the Height count. | |
225 uint16_t height_count = 0; | |
226 if (!subtable.ReadU16(&height_count)) { | |
227 return OTS_FAILURE(); | |
228 } | |
229 | |
230 // Check the Correction Heights. | |
231 for (unsigned i = 0; i < height_count; ++i) { | |
232 if (!ParseMathValueRecord(file, &subtable, data, length)) { | |
233 return OTS_FAILURE(); | |
234 } | |
235 } | |
236 | |
237 // Check the Kern Values. | |
238 for (unsigned i = 0; i <= height_count; ++i) { | |
239 if (!ParseMathValueRecord(file, &subtable, data, length)) { | |
240 return OTS_FAILURE(); | |
241 } | |
242 } | |
243 | |
244 return true; | |
245 } | |
246 | |
247 bool ParseMathKernInfoTable(const ots::OpenTypeFile *file, | |
248 const uint8_t *data, size_t length, | |
249 const uint16_t num_glyphs) { | |
250 ots::Buffer subtable(data, length); | |
251 | |
252 // Check the header. | |
253 uint16_t offset_coverage = 0; | |
254 uint16_t sequence_count = 0; | |
255 if (!subtable.ReadU16(&offset_coverage) || | |
256 !subtable.ReadU16(&sequence_count)) { | |
257 return OTS_FAILURE(); | |
258 } | |
259 | |
260 const unsigned sequence_end = static_cast<unsigned>(2 * 2) + | |
261 sequence_count * 4 * 2; | |
262 if (sequence_end > std::numeric_limits<uint16_t>::max()) { | |
263 return OTS_FAILURE(); | |
264 } | |
265 | |
266 // Check coverage table. | |
267 if (offset_coverage < sequence_end || offset_coverage >= length) { | |
268 return OTS_FAILURE(); | |
269 } | |
270 if (!ots::ParseCoverageTable(file, data + offset_coverage, length - offset_cov
erage, | |
271 num_glyphs, sequence_count)) { | |
272 return OTS_FAILURE(); | |
273 } | |
274 | |
275 // Check sequence of MathKernInfoRecord | |
276 for (unsigned i = 0; i < sequence_count; ++i) { | |
277 // Check TopRight, TopLeft, BottomRight and BottomLeft Math Kern. | |
278 for (unsigned j = 0; j < 4; ++j) { | |
279 uint16_t offset_math_kern = 0; | |
280 if (!subtable.ReadU16(&offset_math_kern)) { | |
281 return OTS_FAILURE(); | |
282 } | |
283 if (offset_math_kern) { | |
284 if (offset_math_kern < sequence_end || offset_math_kern >= length || | |
285 !ParseMathKernTable(file, data + offset_math_kern, | |
286 length - offset_math_kern)) { | |
287 return OTS_FAILURE(); | |
288 } | |
289 } | |
290 } | |
291 } | |
292 | |
293 return true; | |
294 } | |
295 | |
296 bool ParseMathGlyphInfoTable(const ots::OpenTypeFile *file, | |
297 const uint8_t *data, size_t length, | |
298 const uint16_t num_glyphs) { | |
299 ots::Buffer subtable(data, length); | |
300 | |
301 // Check Header. | |
302 uint16_t offset_math_italics_correction_info = 0; | |
303 uint16_t offset_math_top_accent_attachment = 0; | |
304 uint16_t offset_extended_shaped_coverage = 0; | |
305 uint16_t offset_math_kern_info = 0; | |
306 if (!subtable.ReadU16(&offset_math_italics_correction_info) || | |
307 !subtable.ReadU16(&offset_math_top_accent_attachment) || | |
308 !subtable.ReadU16(&offset_extended_shaped_coverage) || | |
309 !subtable.ReadU16(&offset_math_kern_info)) { | |
310 return OTS_FAILURE(); | |
311 } | |
312 | |
313 // Check subtables. | |
314 // The specification does not say whether the offsets for | |
315 // MathItalicsCorrectionInfo, MathTopAccentAttachment and MathKernInfo may | |
316 // be NULL, but that's the case in some fonts (e.g STIX) so we accept that. | |
317 if (offset_math_italics_correction_info) { | |
318 if (offset_math_italics_correction_info >= length || | |
319 offset_math_italics_correction_info < kMathGlyphInfoHeaderSize || | |
320 !ParseMathItalicsCorrectionInfoTable( | |
321 file, data + offset_math_italics_correction_info, | |
322 length - offset_math_italics_correction_info, | |
323 num_glyphs)) { | |
324 return OTS_FAILURE(); | |
325 } | |
326 } | |
327 if (offset_math_top_accent_attachment) { | |
328 if (offset_math_top_accent_attachment >= length || | |
329 offset_math_top_accent_attachment < kMathGlyphInfoHeaderSize || | |
330 !ParseMathTopAccentAttachmentTable(file, data + | |
331 offset_math_top_accent_attachment, | |
332 length - | |
333 offset_math_top_accent_attachment, | |
334 num_glyphs)) { | |
335 return OTS_FAILURE(); | |
336 } | |
337 } | |
338 if (offset_extended_shaped_coverage) { | |
339 if (offset_extended_shaped_coverage >= length || | |
340 offset_extended_shaped_coverage < kMathGlyphInfoHeaderSize || | |
341 !ots::ParseCoverageTable(file, data + offset_extended_shaped_coverage, | |
342 length - offset_extended_shaped_coverage, | |
343 num_glyphs)) { | |
344 return OTS_FAILURE(); | |
345 } | |
346 } | |
347 if (offset_math_kern_info) { | |
348 if (offset_math_kern_info >= length || | |
349 offset_math_kern_info < kMathGlyphInfoHeaderSize || | |
350 !ParseMathKernInfoTable(file, data + offset_math_kern_info, | |
351 length - offset_math_kern_info, num_glyphs)) { | |
352 return OTS_FAILURE(); | |
353 } | |
354 } | |
355 | |
356 return true; | |
357 } | |
358 | |
359 bool ParseGlyphAssemblyTable(const ots::OpenTypeFile *file, | |
360 const uint8_t *data, | |
361 size_t length, const uint16_t num_glyphs) { | |
362 ots::Buffer subtable(data, length); | |
363 | |
364 // Check the header. | |
365 uint16_t part_count = 0; | |
366 if (!ParseMathValueRecord(file, &subtable, data, length) || | |
367 !subtable.ReadU16(&part_count)) { | |
368 return OTS_FAILURE(); | |
369 } | |
370 | |
371 const unsigned sequence_end = kMathValueRecordSize + | |
372 static_cast<unsigned>(2) + part_count * kGlyphPartRecordSize; | |
373 if (sequence_end > std::numeric_limits<uint16_t>::max()) { | |
374 return OTS_FAILURE(); | |
375 } | |
376 | |
377 // Check the sequence of GlyphPartRecord. | |
378 for (unsigned i = 0; i < part_count; ++i) { | |
379 uint16_t glyph = 0; | |
380 uint16_t part_flags = 0; | |
381 if (!subtable.ReadU16(&glyph) || | |
382 !subtable.Skip(2 * 3) || | |
383 !subtable.ReadU16(&part_flags)) { | |
384 return OTS_FAILURE(); | |
385 } | |
386 if (glyph >= num_glyphs) { | |
387 return OTS_FAILURE_MSG("bad glyph ID: %u", glyph); | |
388 } | |
389 if (part_flags & ~0x00000001) { | |
390 return OTS_FAILURE_MSG("unknown part flag: %u", part_flags); | |
391 } | |
392 } | |
393 | |
394 return true; | |
395 } | |
396 | |
397 bool ParseMathGlyphConstructionTable(const ots::OpenTypeFile *file, | |
398 const uint8_t *data, | |
399 size_t length, const uint16_t num_glyphs) { | |
400 ots::Buffer subtable(data, length); | |
401 | |
402 // Check the header. | |
403 uint16_t offset_glyph_assembly = 0; | |
404 uint16_t variant_count = 0; | |
405 if (!subtable.ReadU16(&offset_glyph_assembly) || | |
406 !subtable.ReadU16(&variant_count)) { | |
407 return OTS_FAILURE(); | |
408 } | |
409 | |
410 const unsigned sequence_end = static_cast<unsigned>(2 * 2) + | |
411 variant_count * 2 * 2; | |
412 if (sequence_end > std::numeric_limits<uint16_t>::max()) { | |
413 return OTS_FAILURE(); | |
414 } | |
415 | |
416 // Check the GlyphAssembly offset. | |
417 if (offset_glyph_assembly) { | |
418 if (offset_glyph_assembly >= length || | |
419 offset_glyph_assembly < sequence_end) { | |
420 return OTS_FAILURE(); | |
421 } | |
422 if (!ParseGlyphAssemblyTable(file, data + offset_glyph_assembly, | |
423 length - offset_glyph_assembly, num_glyphs)) { | |
424 return OTS_FAILURE(); | |
425 } | |
426 } | |
427 | |
428 // Check the sequence of MathGlyphVariantRecord. | |
429 for (unsigned i = 0; i < variant_count; ++i) { | |
430 uint16_t glyph = 0; | |
431 if (!subtable.ReadU16(&glyph) || | |
432 !subtable.Skip(2)) { | |
433 return OTS_FAILURE(); | |
434 } | |
435 if (glyph >= num_glyphs) { | |
436 return OTS_FAILURE_MSG("bad glyph ID: %u", glyph); | |
437 } | |
438 } | |
439 | |
440 return true; | |
441 } | |
442 | |
443 bool ParseMathGlyphConstructionSequence(const ots::OpenTypeFile *file, | |
444 ots::Buffer* subtable, | |
445 const uint8_t *data, | |
446 size_t length, | |
447 const uint16_t num_glyphs, | |
448 uint16_t offset_coverage, | |
449 uint16_t glyph_count, | |
450 const unsigned sequence_end) { | |
451 // Check coverage table. | |
452 if (offset_coverage < sequence_end || offset_coverage >= length) { | |
453 return OTS_FAILURE(); | |
454 } | |
455 if (!ots::ParseCoverageTable(file, data + offset_coverage, | |
456 length - offset_coverage, | |
457 num_glyphs, glyph_count)) { | |
458 return OTS_FAILURE(); | |
459 } | |
460 | |
461 // Check sequence of MathGlyphConstruction. | |
462 for (unsigned i = 0; i < glyph_count; ++i) { | |
463 uint16_t offset_glyph_construction = 0; | |
464 if (!subtable->ReadU16(&offset_glyph_construction)) { | |
465 return OTS_FAILURE(); | |
466 } | |
467 if (offset_glyph_construction < sequence_end || | |
468 offset_glyph_construction >= length || | |
469 !ParseMathGlyphConstructionTable(file, data + offset_glyph_constructio
n, | |
470 length - offset_glyph_construction, | |
471 num_glyphs)) { | |
472 return OTS_FAILURE(); | |
473 } | |
474 } | |
475 | |
476 return true; | |
477 } | |
478 | |
479 bool ParseMathVariantsTable(const ots::OpenTypeFile *file, | |
480 const uint8_t *data, | |
481 size_t length, const uint16_t num_glyphs) { | |
482 ots::Buffer subtable(data, length); | |
483 | |
484 // Check the header. | |
485 uint16_t offset_vert_glyph_coverage = 0; | |
486 uint16_t offset_horiz_glyph_coverage = 0; | |
487 uint16_t vert_glyph_count = 0; | |
488 uint16_t horiz_glyph_count = 0; | |
489 if (!subtable.Skip(2) || // MinConnectorOverlap | |
490 !subtable.ReadU16(&offset_vert_glyph_coverage) || | |
491 !subtable.ReadU16(&offset_horiz_glyph_coverage) || | |
492 !subtable.ReadU16(&vert_glyph_count) || | |
493 !subtable.ReadU16(&horiz_glyph_count)) { | |
494 return OTS_FAILURE(); | |
495 } | |
496 | |
497 const unsigned sequence_end = 5 * 2 + vert_glyph_count * 2 + | |
498 horiz_glyph_count * 2; | |
499 if (sequence_end > std::numeric_limits<uint16_t>::max()) { | |
500 return OTS_FAILURE(); | |
501 } | |
502 | |
503 if (!ParseMathGlyphConstructionSequence(file, &subtable, data, length, num_gly
phs, | |
504 offset_vert_glyph_coverage, | |
505 vert_glyph_count, | |
506 sequence_end) || | |
507 !ParseMathGlyphConstructionSequence(file, &subtable, data, length, num_gly
phs, | |
508 offset_horiz_glyph_coverage, | |
509 horiz_glyph_count, | |
510 sequence_end)) { | |
511 return OTS_FAILURE(); | |
512 } | |
513 | |
514 return true; | |
515 } | |
516 | |
517 } // namespace | |
518 | |
519 #define DROP_THIS_TABLE(msg_) \ | |
520 do { \ | |
521 OTS_FAILURE_MSG(msg_ ", table discarded"); \ | |
522 file->math->data = 0; \ | |
523 file->math->length = 0; \ | |
524 } while (0) | |
525 | |
526 namespace ots { | |
527 | |
528 bool ots_math_parse(OpenTypeFile *file, const uint8_t *data, size_t length) { | |
529 // Grab the number of glyphs in the file from the maxp table to check | |
530 // GlyphIDs in MATH table. | |
531 if (!file->maxp) { | |
532 return OTS_FAILURE(); | |
533 } | |
534 const uint16_t num_glyphs = file->maxp->num_glyphs; | |
535 | |
536 Buffer table(data, length); | |
537 | |
538 OpenTypeMATH* math = new OpenTypeMATH; | |
539 file->math = math; | |
540 | |
541 uint32_t version = 0; | |
542 if (!table.ReadU32(&version)) { | |
543 return OTS_FAILURE(); | |
544 } | |
545 if (version != 0x00010000) { | |
546 DROP_THIS_TABLE("bad MATH version"); | |
547 return true; | |
548 } | |
549 | |
550 uint16_t offset_math_constants = 0; | |
551 uint16_t offset_math_glyph_info = 0; | |
552 uint16_t offset_math_variants = 0; | |
553 if (!table.ReadU16(&offset_math_constants) || | |
554 !table.ReadU16(&offset_math_glyph_info) || | |
555 !table.ReadU16(&offset_math_variants)) { | |
556 return OTS_FAILURE(); | |
557 } | |
558 | |
559 if (offset_math_constants >= length || | |
560 offset_math_constants < kMathHeaderSize || | |
561 offset_math_glyph_info >= length || | |
562 offset_math_glyph_info < kMathHeaderSize || | |
563 offset_math_variants >= length || | |
564 offset_math_variants < kMathHeaderSize) { | |
565 DROP_THIS_TABLE("bad offset in MATH header"); | |
566 return true; | |
567 } | |
568 | |
569 if (!ParseMathConstantsTable(file, data + offset_math_constants, | |
570 length - offset_math_constants)) { | |
571 DROP_THIS_TABLE("failed to parse MathConstants table"); | |
572 return true; | |
573 } | |
574 if (!ParseMathGlyphInfoTable(file, data + offset_math_glyph_info, | |
575 length - offset_math_glyph_info, num_glyphs)) { | |
576 DROP_THIS_TABLE("failed to parse MathGlyphInfo table"); | |
577 return true; | |
578 } | |
579 if (!ParseMathVariantsTable(file, data + offset_math_variants, | |
580 length - offset_math_variants, num_glyphs)) { | |
581 DROP_THIS_TABLE("failed to parse MathVariants table"); | |
582 return true; | |
583 } | |
584 | |
585 math->data = data; | |
586 math->length = length; | |
587 return true; | |
588 } | |
589 | |
590 bool ots_math_should_serialise(OpenTypeFile *file) { | |
591 return file->math != NULL && file->math->data != NULL; | |
592 } | |
593 | |
594 bool ots_math_serialise(OTSStream *out, OpenTypeFile *file) { | |
595 if (!out->Write(file->math->data, file->math->length)) { | |
596 return OTS_FAILURE(); | |
597 } | |
598 | |
599 return true; | |
600 } | |
601 | |
602 void ots_math_free(OpenTypeFile *file) { | |
603 delete file->math; | |
604 } | |
605 | |
606 } // namespace ots | |
607 | |
608 #undef TABLE_NAME | |
609 #undef DROP_THIS_TABLE | |
OLD | NEW |