Author: tilman Date: Wed Oct 8 12:23:12 2025 New Revision: 1929016 Log: PDFBOX-5660: optimize, as suggested by Valery Bokov; closes #280
Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1CFont.java Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1CFont.java ============================================================================== --- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1CFont.java Wed Oct 8 12:23:08 2025 (r1929015) +++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/font/PDType1CFont.java Wed Oct 8 12:23:12 2025 (r1929016) @@ -1,491 +1,492 @@ -/* - * 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. - */ - -package org.apache.pdfbox.pdmodel.font; - -import java.awt.geom.AffineTransform; -import java.awt.geom.GeneralPath; -import java.awt.geom.Point2D; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.LogManager; -import org.apache.fontbox.EncodedFont; -import org.apache.fontbox.FontBoxFont; -import org.apache.fontbox.cff.CFFFont; -import org.apache.fontbox.cff.CFFParser; -import org.apache.fontbox.cff.CFFType1Font; -import org.apache.fontbox.util.BoundingBox; -import org.apache.pdfbox.cos.COSDictionary; -import org.apache.pdfbox.cos.COSName; -import org.apache.pdfbox.io.RandomAccessRead; -import org.apache.pdfbox.pdmodel.common.PDRectangle; -import org.apache.pdfbox.pdmodel.common.PDStream; -import org.apache.pdfbox.pdmodel.font.encoding.Encoding; -import org.apache.pdfbox.pdmodel.font.encoding.StandardEncoding; -import org.apache.pdfbox.pdmodel.font.encoding.Type1Encoding; -import org.apache.pdfbox.util.Matrix; - - -import static org.apache.pdfbox.pdmodel.font.UniUtil.getUniNameOfCodePoint; - -/** - * Type 1-equivalent CFF font. - * - * @author Villu Ruusmann - * @author John Hewson - */ -public class PDType1CFont extends PDSimpleFont implements PDVectorFont -{ - private static final Logger LOG = LogManager.getLogger(PDType1CFont.class); - - private final Map<String, Float> glyphHeights = new HashMap<>(); - private final AffineTransform fontMatrixTransform; - private final CFFType1Font cffFont; // embedded font - private final FontBoxFont genericFont; // embedded or system font for rendering - private final boolean isEmbedded; - private final boolean isDamaged; - private Float avgWidth = null; - private Matrix fontMatrix; - private BoundingBox fontBBox; - - /** - * Constructor. - * - * @param fontDictionary the corresponding dictionary - * @throws IOException it something went wrong - */ - public PDType1CFont(COSDictionary fontDictionary) throws IOException - { - super(fontDictionary); - - boolean fontIsDamaged = false; - CFFType1Font cffEmbedded = null; - PDFontDescriptor fd = getFontDescriptor(); - if (fd != null) - { - PDStream ff3Stream = fd.getFontFile3(); - if (ff3Stream != null) - { - try (RandomAccessRead randomAccessRead = fd.getFontFile3().getCOSObject() - .createView()) - { - if (randomAccessRead.length() == 0) - { - LOG.error("Invalid data for embedded Type1C font {}", getName()); - } - else - { - // note: this could be an OpenType file, fortunately CFFParser can handle that - CFFParser cffParser = new CFFParser(); - CFFFont parsedCffFont = cffParser.parse(randomAccessRead).get(0); - if (parsedCffFont instanceof CFFType1Font) - { - cffEmbedded = (CFFType1Font) parsedCffFont; - } - else - { - LOG.error("Expected CFFType1Font, got {}", - parsedCffFont.getClass().getSimpleName()); - fontIsDamaged = true; - } - } - } - catch (IOException e) - { - LOG.error(() -> "Can't read the embedded Type1C font " + getName(), e); - fontIsDamaged = true; - } - } - } - isDamaged = fontIsDamaged; - cffFont = cffEmbedded; - if (cffFont != null) - { - genericFont = cffFont; - isEmbedded = true; - } - else - { - FontMapping<FontBoxFont> mapping = FontMappers.instance() - .getFontBoxFont(getBaseFont(), fd); - genericFont = mapping.getFont(); - - if (mapping.isFallback()) - { - LOG.warn("Using fallback font {} for {}", genericFont.getName(), getBaseFont()); - } - isEmbedded = false; - } - readEncoding(); - fontMatrixTransform = getFontMatrix().createAffineTransform(); - fontMatrixTransform.scale(1000, 1000); - } - - @Override - public FontBoxFont getFontBoxFont() - { - return genericFont; - } - - /** - * Returns the PostScript name of the font. - * - * @return the PostScript name of the font - */ - public final String getBaseFont() - { - return dict.getNameAsString(COSName.BASE_FONT); - } - - @Override - public GeneralPath getPath(String name) throws IOException - { - // Acrobat only draws .notdef for embedded or "Standard 14" fonts, see PDFBOX-2372 - if (name.equals(".notdef") && !isEmbedded() && !isStandard14()) - { - return new GeneralPath(); - } - if ("sfthyphen".equals(name)) - { - return genericFont.getPath("hyphen"); - } - if ("nbspace".equals(name)) - { - if (!hasGlyph("space")) - { - return new GeneralPath(); - } - return genericFont.getPath("space"); - } - return genericFont.getPath(name); - } - - @Override - public boolean hasGlyph(int code) throws IOException - { - String name = getEncoding().getName(code); - name = getNameInFont(name); - if ("sfthyphen".equals(name)) - { - return hasGlyph("hyphen"); - } - if ("nbspace".equals(name)) - { - return hasGlyph("space"); - } - return hasGlyph(name); - } - - @Override - public GeneralPath getPath(int code) throws IOException - { - String name = getEncoding().getName(code); - name = getNameInFont(name); - if ("sfthyphen".equals(name)) - { - return getPath("hyphen"); - } - if ("nbspace".equals(name)) - { - if (!hasGlyph("space")) - { - return new GeneralPath(); - } - return getPath("space"); - } - return getPath(name); - } - - @Override - public GeneralPath getNormalizedPath(int code) throws IOException - { - String name = getEncoding().getName(code); - name = getNameInFont(name); - if ("nbspace".equals(name)) - { - if (!hasGlyph("space")) - { - return new GeneralPath(); - } - name = "space"; - } - else if ("sfthyphen".equals(name)) - { - name = "hyphen"; - } - GeneralPath path = getPath(name); - if (path == null) - { - return getPath(".notdef"); - } - return path; - } - - @Override - public boolean hasGlyph(String name) throws IOException - { - return genericFont.hasGlyph(name); - } - - @Override - public final String getName() - { - return getBaseFont(); - } - - @Override - public BoundingBox getBoundingBox() throws IOException - { - if (fontBBox == null) - { - fontBBox = generateBoundingBox(); - } - return fontBBox; - } - - private BoundingBox generateBoundingBox() throws IOException - { - if (getFontDescriptor() != null) { - PDRectangle bbox = getFontDescriptor().getFontBoundingBox(); - if (isNonZeroBoundingBox(bbox)) - { - return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), - bbox.getUpperRightX(), bbox.getUpperRightY()); - } - } - return genericFont.getFontBBox(); - } - - //@Override - public String codeToName(int code) - { - return getEncoding().getName(code); - } - - @Override - protected Encoding readEncodingFromFont() throws IOException - { - if (!isEmbedded() && getStandard14AFM() != null) - { - // read from AFM - return new Type1Encoding(getStandard14AFM()); - } - else - { - // extract from Type1 font/substitute - if (genericFont instanceof EncodedFont) - { - return Type1Encoding.fromFontBox(((EncodedFont) genericFont).getEncoding()); - } - else - { - // default (only happens with TTFs) - return StandardEncoding.INSTANCE; - } - } - } - - @Override - public int readCode(InputStream in) throws IOException - { - return in.read(); - } - - @Override - public final Matrix getFontMatrix() - { - if (fontMatrix == null) - { - List<Number> numbers = null; - try - { - numbers = genericFont.getFontMatrix(); - } - catch (IOException e) - { - LOG.debug("Couldn't get font matrix - returning default value", e); - fontMatrix = DEFAULT_FONT_MATRIX; - } - - if (numbers != null && numbers.size() == 6) - { - fontMatrix = new Matrix( - numbers.get(0).floatValue(), numbers.get(1).floatValue(), - numbers.get(2).floatValue(), numbers.get(3).floatValue(), - numbers.get(4).floatValue(), numbers.get(5).floatValue()); - } - else - { - return super.getFontMatrix(); - } - } - return fontMatrix; - } - - @Override - public boolean isDamaged() - { - return isDamaged; - } - - @Override - public float getWidthFromFont(int code) throws IOException - { - String name = codeToName(code); - name = getNameInFont(name); - float width = genericFont.getWidth(name); - - Point2D p = new Point2D.Float(width, 0); - fontMatrixTransform.transform(p, p); - return (float)p.getX(); - } - - @Override - public boolean isEmbedded() - { - return isEmbedded; - } - - @Override - public float getHeight(int code) throws IOException - { - String name = codeToName(code); - float height; - if (!glyphHeights.containsKey(name)) - { - if (cffFont == null) - { - LOG.warn("No embedded CFF font, returning 0"); - return 0; - } - height = (float) cffFont.getType1CharString(name).getBounds().getHeight(); - glyphHeights.put(name, height); - } - else - { - height = glyphHeights.get(name); - } - return height; - } - - @Override - protected byte[] encode(int unicode) throws IOException - { - String name = getGlyphList().codePointToName(unicode); - if (!encoding.contains(name)) - { - throw new IllegalArgumentException( - String.format("U+%04X ('%s') is not available in font %s encoding: %s", - unicode, name, getName(), encoding.getEncodingName())); - } - - String nameInFont = getNameInFont(name); - - Map<String, Integer> inverted = encoding.getNameToCodeMap(); - - if (nameInFont.equals(".notdef") || !genericFont.hasGlyph(nameInFont)) - { - throw new IllegalArgumentException( - String.format("No glyph for U+%04X in font %s", unicode, getName())); - } - - int code = inverted.get(name); - return new byte[] { (byte)code }; - } - - @Override - public float getStringWidth(String string) throws IOException - { - if (cffFont == null) - { - LOG.warn("No embedded CFF font, returning 0"); - return 0; - } - float width = 0; - for (int i = 0; i < string.length(); i++) - { - int codePoint = string.codePointAt(i); - String name = getGlyphList().codePointToName(codePoint); - if (!cffFont.hasGlyph(name)) - { - throw new IllegalArgumentException( - String.format("U+%04X ('%s') is not available in font %s", - codePoint, name, getName())); - } - width += cffFont.getType1CharString(name).getWidth(); - } - return width; - } - - @Override - public float getAverageFontWidth() - { - if (avgWidth == null) - { - avgWidth = getAverageCharacterWidth(); - } - return avgWidth; - } - - /** - * Returns the embedded Type 1-equivalent CFF font. - * - * @return the cffFont - */ - public CFFType1Font getCFFType1Font() - { - return cffFont; - } - - // todo: this is a replacement for FontMetrics method - private float getAverageCharacterWidth() - { - // todo: not implemented, highly suspect - return 500; - } - - /** - * Maps a PostScript glyph name to the name in the underlying font, for example when - * using a TTF font we might map "W" to "uni0057". - */ - private String getNameInFont(String name) throws IOException - { - if (isEmbedded() || genericFont.hasGlyph(name)) - { - return name; - } - else - { - // try unicode name - String unicodes = getGlyphList().toUnicode(name); - if (unicodes != null && unicodes.length() == 1) - { - String uniName = getUniNameOfCodePoint(unicodes.codePointAt(0)); - if (genericFont.hasGlyph(uniName)) - { - return uniName; - } - } - } - return ".notdef"; - } - -} +/* + * 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. + */ + +package org.apache.pdfbox.pdmodel.font; + +import java.awt.geom.AffineTransform; +import java.awt.geom.GeneralPath; +import java.awt.geom.Point2D; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.LogManager; +import org.apache.fontbox.EncodedFont; +import org.apache.fontbox.FontBoxFont; +import org.apache.fontbox.cff.CFFFont; +import org.apache.fontbox.cff.CFFParser; +import org.apache.fontbox.cff.CFFType1Font; +import org.apache.fontbox.util.BoundingBox; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.io.RandomAccessRead; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.font.encoding.Encoding; +import org.apache.pdfbox.pdmodel.font.encoding.StandardEncoding; +import org.apache.pdfbox.pdmodel.font.encoding.Type1Encoding; +import org.apache.pdfbox.util.Matrix; + + +import static org.apache.pdfbox.pdmodel.font.UniUtil.getUniNameOfCodePoint; + +/** + * Type 1-equivalent CFF font. + * + * @author Villu Ruusmann + * @author John Hewson + */ +public class PDType1CFont extends PDSimpleFont implements PDVectorFont +{ + private static final Logger LOG = LogManager.getLogger(PDType1CFont.class); + + private final Map<String, Float> glyphHeights = new HashMap<>(); + private final AffineTransform fontMatrixTransform; + private final CFFType1Font cffFont; // embedded font + private final FontBoxFont genericFont; // embedded or system font for rendering + private final boolean isEmbedded; + private final boolean isDamaged; + private Float avgWidth = null; + private Matrix fontMatrix; + private BoundingBox fontBBox; + + /** + * Constructor. + * + * @param fontDictionary the corresponding dictionary + * @throws IOException it something went wrong + */ + public PDType1CFont(COSDictionary fontDictionary) throws IOException + { + super(fontDictionary); + + boolean fontIsDamaged = false; + CFFType1Font cffEmbedded = null; + PDFontDescriptor fd = getFontDescriptor(); + if (fd != null) + { + PDStream ff3Stream = fd.getFontFile3(); + if (ff3Stream != null) + { + try (RandomAccessRead randomAccessRead = ff3Stream.getCOSObject() + .createView()) + { + if (randomAccessRead.length() == 0) + { + LOG.error("Invalid data for embedded Type1C font {}", getName()); + } + else + { + // note: this could be an OpenType file, fortunately CFFParser can handle that + CFFParser cffParser = new CFFParser(); + CFFFont parsedCffFont = cffParser.parse(randomAccessRead).get(0); + if (parsedCffFont instanceof CFFType1Font) + { + cffEmbedded = (CFFType1Font) parsedCffFont; + } + else + { + LOG.error("Expected CFFType1Font, got {}", + parsedCffFont.getClass().getSimpleName()); + fontIsDamaged = true; + } + } + } + catch (IOException e) + { + LOG.error(() -> "Can't read the embedded Type1C font " + getName(), e); + fontIsDamaged = true; + } + } + } + isDamaged = fontIsDamaged; + cffFont = cffEmbedded; + if (cffFont != null) + { + genericFont = cffFont; + isEmbedded = true; + } + else + { + String baseFont = getBaseFont(); + FontMapping<FontBoxFont> mapping = FontMappers.instance() + .getFontBoxFont(baseFont, fd); + genericFont = mapping.getFont(); + + if (mapping.isFallback()) + { + LOG.warn("Using fallback font {} for {}", genericFont.getName(), baseFont); + } + isEmbedded = false; + } + readEncoding(); + fontMatrixTransform = getFontMatrix().createAffineTransform(); + fontMatrixTransform.scale(1000, 1000); + } + + @Override + public FontBoxFont getFontBoxFont() + { + return genericFont; + } + + /** + * Returns the PostScript name of the font. + * + * @return the PostScript name of the font + */ + public final String getBaseFont() + { + return dict.getNameAsString(COSName.BASE_FONT); + } + + @Override + public GeneralPath getPath(String name) throws IOException + { + // Acrobat only draws .notdef for embedded or "Standard 14" fonts, see PDFBOX-2372 + if (name.equals(".notdef") && !isEmbedded() && !isStandard14()) + { + return new GeneralPath(); + } + if ("sfthyphen".equals(name)) + { + return genericFont.getPath("hyphen"); + } + if ("nbspace".equals(name)) + { + if (!hasGlyph("space")) + { + return new GeneralPath(); + } + return genericFont.getPath("space"); + } + return genericFont.getPath(name); + } + + @Override + public boolean hasGlyph(int code) throws IOException + { + String name = getEncoding().getName(code); + name = getNameInFont(name); + if ("sfthyphen".equals(name)) + { + return hasGlyph("hyphen"); + } + if ("nbspace".equals(name)) + { + return hasGlyph("space"); + } + return hasGlyph(name); + } + + @Override + public GeneralPath getPath(int code) throws IOException + { + String name = getEncoding().getName(code); + name = getNameInFont(name); + if ("sfthyphen".equals(name)) + { + return getPath("hyphen"); + } + if ("nbspace".equals(name)) + { + if (!hasGlyph("space")) + { + return new GeneralPath(); + } + return getPath("space"); + } + return getPath(name); + } + + @Override + public GeneralPath getNormalizedPath(int code) throws IOException + { + String name = getEncoding().getName(code); + name = getNameInFont(name); + if ("nbspace".equals(name)) + { + if (!hasGlyph("space")) + { + return new GeneralPath(); + } + name = "space"; + } + else if ("sfthyphen".equals(name)) + { + name = "hyphen"; + } + GeneralPath path = getPath(name); + if (path == null) + { + return getPath(".notdef"); + } + return path; + } + + @Override + public boolean hasGlyph(String name) throws IOException + { + return genericFont.hasGlyph(name); + } + + @Override + public final String getName() + { + return getBaseFont(); + } + + @Override + public BoundingBox getBoundingBox() throws IOException + { + if (fontBBox == null) + { + fontBBox = generateBoundingBox(); + } + return fontBBox; + } + + private BoundingBox generateBoundingBox() throws IOException + { + if (getFontDescriptor() != null) { + PDRectangle bbox = getFontDescriptor().getFontBoundingBox(); + if (isNonZeroBoundingBox(bbox)) + { + return new BoundingBox(bbox.getLowerLeftX(), bbox.getLowerLeftY(), + bbox.getUpperRightX(), bbox.getUpperRightY()); + } + } + return genericFont.getFontBBox(); + } + + //@Override + public String codeToName(int code) + { + return getEncoding().getName(code); + } + + @Override + protected Encoding readEncodingFromFont() throws IOException + { + if (!isEmbedded() && getStandard14AFM() != null) + { + // read from AFM + return new Type1Encoding(getStandard14AFM()); + } + else + { + // extract from Type1 font/substitute + if (genericFont instanceof EncodedFont) + { + return Type1Encoding.fromFontBox(((EncodedFont) genericFont).getEncoding()); + } + else + { + // default (only happens with TTFs) + return StandardEncoding.INSTANCE; + } + } + } + + @Override + public int readCode(InputStream in) throws IOException + { + return in.read(); + } + + @Override + public final Matrix getFontMatrix() + { + if (fontMatrix == null) + { + List<Number> numbers = null; + try + { + numbers = genericFont.getFontMatrix(); + } + catch (IOException e) + { + LOG.debug("Couldn't get font matrix - returning default value", e); + fontMatrix = DEFAULT_FONT_MATRIX; + } + + if (numbers != null && numbers.size() == 6) + { + fontMatrix = new Matrix( + numbers.get(0).floatValue(), numbers.get(1).floatValue(), + numbers.get(2).floatValue(), numbers.get(3).floatValue(), + numbers.get(4).floatValue(), numbers.get(5).floatValue()); + } + else + { + return super.getFontMatrix(); + } + } + return fontMatrix; + } + + @Override + public boolean isDamaged() + { + return isDamaged; + } + + @Override + public float getWidthFromFont(int code) throws IOException + { + String name = codeToName(code); + name = getNameInFont(name); + float width = genericFont.getWidth(name); + + Point2D p = new Point2D.Float(width, 0); + fontMatrixTransform.transform(p, p); + return (float)p.getX(); + } + + @Override + public boolean isEmbedded() + { + return isEmbedded; + } + + @Override + public float getHeight(int code) throws IOException + { + String name = codeToName(code); + float height; + if (!glyphHeights.containsKey(name)) + { + if (cffFont == null) + { + LOG.warn("No embedded CFF font, returning 0"); + return 0; + } + height = (float) cffFont.getType1CharString(name).getBounds().getHeight(); + glyphHeights.put(name, height); + } + else + { + height = glyphHeights.get(name); + } + return height; + } + + @Override + protected byte[] encode(int unicode) throws IOException + { + String name = getGlyphList().codePointToName(unicode); + if (!encoding.contains(name)) + { + throw new IllegalArgumentException( + String.format("U+%04X ('%s') is not available in font %s encoding: %s", + unicode, name, getName(), encoding.getEncodingName())); + } + + String nameInFont = getNameInFont(name); + + Map<String, Integer> inverted = encoding.getNameToCodeMap(); + + if (nameInFont.equals(".notdef") || !genericFont.hasGlyph(nameInFont)) + { + throw new IllegalArgumentException( + String.format("No glyph for U+%04X in font %s", unicode, getName())); + } + + int code = inverted.get(name); + return new byte[] { (byte)code }; + } + + @Override + public float getStringWidth(String string) throws IOException + { + if (cffFont == null) + { + LOG.warn("No embedded CFF font, returning 0"); + return 0; + } + float width = 0; + for (int i = 0; i < string.length(); i++) + { + int codePoint = string.codePointAt(i); + String name = getGlyphList().codePointToName(codePoint); + if (!cffFont.hasGlyph(name)) + { + throw new IllegalArgumentException( + String.format("U+%04X ('%s') is not available in font %s", + codePoint, name, getName())); + } + width += cffFont.getType1CharString(name).getWidth(); + } + return width; + } + + @Override + public float getAverageFontWidth() + { + if (avgWidth == null) + { + avgWidth = getAverageCharacterWidth(); + } + return avgWidth; + } + + /** + * Returns the embedded Type 1-equivalent CFF font. + * + * @return the cffFont + */ + public CFFType1Font getCFFType1Font() + { + return cffFont; + } + + // todo: this is a replacement for FontMetrics method + private float getAverageCharacterWidth() + { + // todo: not implemented, highly suspect + return 500; + } + + /** + * Maps a PostScript glyph name to the name in the underlying font, for example when + * using a TTF font we might map "W" to "uni0057". + */ + private String getNameInFont(String name) throws IOException + { + if (isEmbedded() || genericFont.hasGlyph(name)) + { + return name; + } + else + { + // try unicode name + String unicodes = getGlyphList().toUnicode(name); + if (unicodes != null && unicodes.length() == 1) + { + String uniName = getUniNameOfCodePoint(unicodes.codePointAt(0)); + if (genericFont.hasGlyph(uniName)) + { + return uniName; + } + } + } + return ".notdef"; + } + +}
