marytts.htsengine.HMMData Maven / Gradle / Ivy
The newest version!
/* ----------------------------------------------------------------- */
/* The HMM-Based Speech Synthesis Engine "hts_engine API" */
/* developed by HTS Working Group */
/* http://hts-engine.sourceforge.net/ */
/* ----------------------------------------------------------------- */
/* */
/* Copyright (c) 2001-2010 Nagoya Institute of Technology */
/* Department of Computer Science */
/* */
/* 2001-2008 Tokyo Institute of Technology */
/* Interdisciplinary Graduate School of */
/* Science and Engineering */
/* */
/* All rights reserved. */
/* */
/* Redistribution and use in source and binary forms, with or */
/* without modification, are permitted provided that the following */
/* conditions are met: */
/* */
/* - Redistributions of source code must retain the above copyright */
/* notice, this list of conditions and the following disclaimer. */
/* - Redistributions in binary form must reproduce the above */
/* copyright notice, this list of conditions and the following */
/* disclaimer in the documentation and/or other materials provided */
/* with the distribution. */
/* - Neither the name of the HTS working group nor the names of its */
/* contributors may be used to endorse or promote products derived */
/* from this software without specific prior written permission. */
/* */
/* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND */
/* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, */
/* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF */
/* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE */
/* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS */
/* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, */
/* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED */
/* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, */
/* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON */
/* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, */
/* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY */
/* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE */
/* POSSIBILITY OF SUCH DAMAGE. */
/* ----------------------------------------------------------------- */
/**
* Copyright 2011 DFKI GmbH.
* All Rights Reserved. Use is subject to license terms.
*
* This file is part of MARY TTS.
*
* MARY TTS is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see .
*
*/
package marytts.htsengine;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Scanner;
import java.util.Set;
import java.util.Vector;
import marytts.config.MaryConfig;
import marytts.exceptions.MaryConfigurationException;
import marytts.features.FeatureDefinition;
import marytts.server.MaryProperties;
import marytts.util.FeatureUtils;
import marytts.htsengine.HMMData.FeatureType;
import marytts.util.MaryUtils;
import marytts.util.io.PropertiesAccessor;
import org.apache.log4j.Logger;
/**
* Configuration files and global variables for HTS engine.
*
* Java port and extension of HTS engine API version 1.04 Extension: mixed excitation
*
* @author Marcela Charfuelan
*/
public class HMMData {
/** Number of model and identificator for the models */
public static final int HTS_NUMMTYPE = 5;
public static enum FeatureType {
DUR, // duration
MGC, // MGC mel-generalized cepstral coefficients
LF0, // log(fundamental frequency)
STR, // strength of excitation?
MAG, // fourier magnitudes for pulse generation
};
public enum PdfFileFormat {
dur, lf0, mgc, str, mag, join
};
private Logger logger = MaryUtils.getLogger("HMMData");
/**
* Global variables for some functions, initialised with default values, so these values can be loaded from a configuration
* file.
*/
private int rate = 16000; /* sampling rate default: 16Khz */
private int fperiod = 80; /* frame period or frame shift (point) default: 0.005sec = rate*0.005 = 80 */
private double rho = 0.0; /* variable for speaking rate control */
/*
* MGC: stage=gamma=0.0 alpha=0.42 linear gain LSP: gamma>0.0 LSP: gamma=1.0 alpha=0.0 Mel-LSP: gamma=1.0 alpha=0.42 MGC-LSP:
* gamma=3.0 alpha=0.42
*/
private int stage = 0; /* defines gamma=-1/stage : if stage=0 then Gamma=0 */
private double alpha = 0.55; // 0.42; /* variable for frequency warping parameter */
private double beta = 0.0; /* variable for postfiltering */
private boolean useLogGain = false; /* log gain flag (for LSP) */
private double uv = 0.5; /* variable for U/V threshold */
private boolean algnst = false; /* use state level alignment for duration */
private boolean algnph = false; /* use phone level alignment for duration */
private boolean useMixExc = true; /* use Mixed Excitation */
private boolean useFourierMag = false; /* use Fourier magnitudes for pulse generation */
/** Global variance (GV) settings */
private boolean useGV = false; /* use global variance in parameter generation */
private boolean useContextDependentGV = false; /* Variable for allowing context-dependent GV for sil */
private boolean gvMethodGradient = true; /* GV method: gradient or derivative (default gradient) */
/* Max number of GV iterations when using gradient method, for derivative 5 is used by default */
private int maxMgcGvIter = 100;
private int maxLf0GvIter = 100;
private int maxStrGvIter = 100;
private int maxMagGvIter = 100;
/* GV weights for each parameter: between 0.0-2.0 */
private double gvWeightMgc = 1.0;
private double gvWeightLf0 = 1.0;
private double gvWeightStr = 1.0;
private double gvWeightMag = 1.0;
private boolean useAcousticModels = false; /* true is using AcousticModeller, is true for MARY 4.1 voices */
/**
* variables for controlling generation of speech in the vocoder these variables have default values but can be fixed and read
* from the audio effects component. [Default][min--max]
*/
private double f0Std = 1.0; /* variable for f0 control, multiply f0 [1.0][0.0--5.0] */
private double f0Mean = 0.0; /* variable for f0 control, add f0 [0.0][0.0--100.0] */
private double length = 0.0; /* total number of frame for generated speech */
/* length of generated speech (in seconds) [N/A][0.0--30.0] */
private double durationScale = 1.0; /* less than 1.0 is faster and more than 1.0 is slower, min=0.1 max=3.0 */
/** Tree files and TreeSet object */
private InputStream treeDurStream; /* durations tree file */
private InputStream treeLf0Stream; /* lf0 tree file */
private InputStream treeMgcStream; /* Mgc tree file */
private InputStream treeStrStream; /* Strengths tree file */
private InputStream treeMagStream; /* Fourier magnitudes tree file */
private FeatureDefinition feaDef; /* The feature definition is used for loading the tree using questions in MARY format */
/**
* CartTreeSet contains the tree-xxx.inf, xxx: dur, lf0, Mgc, str and mag these are all the trees trained for a particular
* voice. the Cart tree also contains the corresponding pdfs.
*/
private CartTreeSet cart = new CartTreeSet();
/** HMM pdf model files and ModelSet object */
private InputStream pdfDurStream; /* durations Pdf file */
private InputStream pdfLf0Stream; /* lf0 Pdf file */
private InputStream pdfMgcStream; /* Mgc Pdf file */
private InputStream pdfStrStream; /* Strengths Pdf file */
private InputStream pdfMagStream; /* Fourier magnitudes Pdf file */
/** GV pdf files */
/** Global variance file, it contains one global mean vector and one global diagonal covariance vector */
private InputStream pdfLf0GVStream; /* lf0 GV pdf file */
private InputStream pdfMgcGVStream; /* Mgc GV pdf file */
private InputStream pdfStrGVStream; /* Str GV pdf file */
private InputStream pdfMagGVStream; /* Mag GV pdf file */
private InputStream switchGVStream; /* File for allowing context dependent GV. */
/* This file contains the phones, sil or pause, for which GV is not calculated (not used yet) */
/* this tree does not have a corresponding pdf file, because it just indicate which labels in context to avoid for GV. */
/** GVModelSet contains the global covariance and mean for lf0, mgc, str and mag */
private GVModelSet gv = new GVModelSet();
/** Variables for mixed excitation */
private int numFilters;
private int orderFilters;
private double mixFilters[][]; /* filters for mixed excitation */
/** tricky phones file if generated during training of HMMs. */
private PhoneTranslator trickyPhones;
public int getRate() {
return rate;
}
public int getFperiod() {
return fperiod;
}
public double getRho() {
return rho;
}
public double getAlpha() {
return alpha;
}
public double getBeta() {
return beta;
}
public int getStage() {
return stage;
}
public double getGamma() {
return (stage != 0) ? -1.0 / stage : 0.0;
}
public boolean getUseLogGain() {
return useLogGain;
}
public double getUV() {
return uv;
}
public boolean getAlgnst() {
return algnst;
}
public boolean getAlgnph() {
return algnph;
}
public double getF0Std() {
return f0Std;
}
public double getF0Mean() {
return f0Mean;
}
public double getLength() {
return length;
}
public double getDurationScale() {
return durationScale;
}
public InputStream getTreeDurStream() {
return treeDurStream;
}
public InputStream getTreeLf0Stream() {
return treeLf0Stream;
}
public InputStream getTreeMgcStream() {
return treeMgcStream;
}
public InputStream getTreeStrStream() {
return treeStrStream;
}
public InputStream getTreeMagStream() {
return treeMagStream;
}
public FeatureDefinition getFeatureDefinition() {
return feaDef;
}
public InputStream getPdfDurStream() {
return pdfDurStream;
}
public InputStream getPdfLf0Stream() {
return pdfLf0Stream;
}
public InputStream getPdfMgcStream() {
return pdfMgcStream;
}
public InputStream getPdfStrStream() {
return pdfStrStream;
}
public InputStream getPdfMagStream() {
return pdfMagStream;
}
public boolean getUseAcousticModels() {
return useAcousticModels;
}
public void setUseAcousticModels(boolean bval) {
useAcousticModels = bval;
}
public boolean getUseMixExc() {
return useMixExc;
}
public boolean getUseFourierMag() {
return useFourierMag;
}
public boolean getUseGV() {
return useGV;
}
public boolean getUseContextDependentGV() {
return useContextDependentGV;
}
public boolean getGvMethodGradient() {
return gvMethodGradient;
}
public int getMaxMgcGvIter() {
return maxMgcGvIter;
}
public int getMaxLf0GvIter() {
return maxLf0GvIter;
}
public int getMaxStrGvIter() {
return maxStrGvIter;
}
public int getMaxMagGvIter() {
return maxMagGvIter;
}
public double getGvWeightMgc() {
return gvWeightMgc;
}
public double getGvWeightLf0() {
return gvWeightLf0;
}
public double getGvWeightStr() {
return gvWeightStr;
}
public double getGvWeightMag() {
return gvWeightMag;
}
public InputStream getPdfLf0GVStream() {
return pdfLf0GVStream;
}
public InputStream getPdfMgcGVStream() {
return pdfMgcGVStream;
}
public InputStream getPdfStrGVStream() {
return pdfStrGVStream;
}
public InputStream getPdfMagGVStream() {
return pdfMagGVStream;
}
public InputStream getSwitchGVStream() {
return switchGVStream;
}
public int getNumFilters() {
return numFilters;
}
public int getOrderFilters() {
return orderFilters;
}
public double[][] getMixFilters() {
return mixFilters;
}
public void setRate(int ival) {
rate = ival;
}
public void setFperiod(int ival) {
fperiod = ival;
}
public void setAlpha(double dval) {
alpha = dval;
}
public void setBeta(double dval) {
beta = dval;
}
public void setStage(int ival) {
stage = ival;
}
public void setUseLogGain(boolean bval) {
useLogGain = bval;
}
/*
* These variables have default values but can be modified with setting in audio effects component.
*/
public void setF0Std(double dval) {
/* default=1.0, min=0.0, max=3.0 */
if (dval >= 0.0 && dval <= 3.0)
f0Std = dval;
else
f0Std = 1.0;
}
public void setF0Mean(double dval) {
/* default=0.0, min=-300.0, max=300.0 */
if (dval >= -300.0 && dval <= 300.0)
f0Mean = dval;
else
f0Mean = 0.0;
}
public void setLength(double dval) {
length = dval;
}
public void setDurationScale(double dval) {
/* default=1.0, min=0.1, max=3.0 */
if (dval >= 0.1 && dval <= 3.0)
durationScale = dval;
else
durationScale = 1.0;
}
public CartTreeSet getCartTreeSet() {
return cart;
}
public GVModelSet getGVModelSet() {
return gv;
}
public void setPdfStrStream(InputStream str) {
pdfStrStream = str;
}
public void setPdfMagStream(InputStream mag) {
pdfMagStream = mag;
}
public void setUseMixExc(boolean bval) {
useMixExc = bval;
}
public void setUseFourierMag(boolean bval) {
useFourierMag = bval;
}
public void setUseGV(boolean bval) {
useGV = bval;
}
public void setUseContextDepenendentGV(boolean bval) {
useContextDependentGV = bval;
}
public void setGvMethod(String sval) {
if (sval.contentEquals("gradient"))
gvMethodGradient = true;
else
gvMethodGradient = false; // then simple derivative method is used
}
public void setMaxMgcGvIter(int val) {
maxMgcGvIter = val;
}
public void setMaxLf0GvIter(int val) {
maxLf0GvIter = val;
}
public void setMaxStrGvIter(int val) {
maxStrGvIter = val;
}
public void setGvWeightMgc(double dval) {
gvWeightMgc = dval;
}
public void setGvWeightLf0(double dval) {
gvWeightLf0 = dval;
}
public void setGvWeightStr(double dval) {
gvWeightStr = dval;
}
public void setNumFilters(int val) {
numFilters = val;
}
public void setOrderFilters(int val) {
orderFilters = val;
}
public void loadCartTreeSet() throws IOException, MaryConfigurationException {
cart.loadTreeSet(this, feaDef, trickyPhones);
}
public void loadGVModelSet() throws IOException {
gv.loadGVModelSet(this, feaDef);
}
public void initHMMData(PropertiesAccessor p, String voiceName) throws IOException, MaryConfigurationException {
logger.debug("Reached new initHMMData");
String prefix = "voice." + voiceName;
rate = p.getInteger(prefix + ".samplingRate", rate);
fperiod = p.getInteger(prefix + ".framePeriod", fperiod);
alpha = p.getDouble(prefix + ".alpha", alpha);
stage = p.getInteger(prefix + ".gamma", stage);
useLogGain = p.getBoolean(prefix + ".logGain", useLogGain);
beta = p.getDouble(prefix + ".beta", beta);
treeDurStream = p.getStream(prefix + ".Ftd"); /* Tree DUR */
treeLf0Stream = p.getStream(prefix + ".Ftf"); /* Tree LF0 */
treeMgcStream = p.getStream(prefix + ".Ftm"); /* Tree MCP */
treeStrStream = p.getStream(prefix + ".Fts"); /* Tree STR */
treeMagStream = p.getStream(prefix + ".Fta"); /* Tree MAG */
pdfDurStream = p.getStream(prefix + ".Fmd"); /* Model DUR */
pdfLf0Stream = p.getStream(prefix + ".Fmf"); /* Model LF0 */
pdfMgcStream = p.getStream(prefix + ".Fmm"); /* Model MCP */
pdfStrStream = p.getStream(prefix + ".Fms"); /* Model STR */
pdfMagStream = p.getStream(prefix + ".Fma"); /* Model MAG */
useAcousticModels = p.getBoolean(prefix + ".useAcousticModels"); /*
* use AcousticModeller, so prosody modification is
* enabled
*/
useMixExc = p.getBoolean(prefix + ".useMixExc"); /* Use Mixed excitation */
useFourierMag = p.getBoolean(prefix + ".useFourierMag"); /* Use Fourier magnitudes for pulse generation */
useGV = p.getBoolean(prefix + ".useGV"); /* Use Global Variance in parameter generation */
if (useGV) {
useContextDependentGV = p.getBoolean(prefix + ".useContextDependentGV"); /* Use context-dependent GV, (gv without sil) */
String gvMethod = p.getProperty(prefix + ".gvMethod"); /* GV method: gradient or derivative (default gradient) */
// this feature is new for MARY 5.0 so it will not appear in old config files
if (gvMethod != null)
setGvMethod(gvMethod);
// Number of iteration for GV
maxMgcGvIter = p.getInteger(prefix + ".maxMgcGvIter", maxMgcGvIter); /*
* Max number of iterations for MGC gv
* optimisation
*/
maxLf0GvIter = p.getInteger(prefix + ".maxLf0GvIter", maxLf0GvIter); /*
* Max number of iterations for LF0 gv
* optimisation
*/
maxStrGvIter = p.getInteger(prefix + ".maxStrGvIter", maxStrGvIter); /*
* Max number of iterations for STR gv
* optimisation
*/
// weights for GV
gvWeightMgc = p.getDouble(prefix + ".gvWeightMgc", gvWeightMgc); /* GV weight for mgc between 0.0-2.0 default 1.0 */
gvWeightLf0 = p.getDouble(prefix + ".gvWeightLf0", gvWeightLf0); /* GV weight for lf0 between 0.0-2.0 default 1.0 */
gvWeightStr = p.getDouble(prefix + ".gvWeightStr", gvWeightStr); /* GV weight for str between 0.0-2.0 default 1.0 */
// GV pdf files: mean and variance (diagonal covariance)
pdfLf0GVStream = p.getStream(prefix + ".Fgvf"); /* GV Model LF0 */
pdfMgcGVStream = p.getStream(prefix + ".Fgvm"); /* GV Model MCP */
pdfStrGVStream = p.getStream(prefix + ".Fgvs"); /* GV Model STR */
pdfMagGVStream = p.getStream(prefix + ".Fgva"); /* GV Model MAG */
}
/* targetfeatures file, for testing */
/* Example context feature file in TARGETFEATURES format */
InputStream featureStream = p.getStream(prefix + ".FeaFile");
feaDef = FeatureUtils.readFeatureDefinition(featureStream);
/* trickyPhones file if any */
trickyPhones = new PhoneTranslator(p.getStream(prefix + ".trickyPhonesFile")); /* tricky phones file, if any */
/* Configuration for mixed excitation */
InputStream mixFiltersStream = p.getStream(prefix + ".Fif"); /* Filter coefficients file for mixed excitation */
if (mixFiltersStream != null) {
numFilters = p.getInteger(prefix + ".in"); /* Number of filters */
logger.debug("Loading Mixed Excitation Filters File:");
readMixedExcitationFilters(mixFiltersStream);
}
/* Load TreeSet in CARTs. */
logger.debug("Loading Tree Set in CARTs:");
loadCartTreeSet();
/* Load GV ModelSet gv */
logger.debug("Loading GV Model Set:");
loadGVModelSet();
logger.debug("InitHMMData complete");
}
/**
* Reads from configuration file all the data files in this class this method is used when running HTSengine stand alone.
*
* @param voiceName
* voiceName
* @param marybase
* marybase
* @param configFile
* configFile
* @throws Exception
* Exception
*/
public void initHMMData(String voiceName, String marybase, String configFile) throws Exception {
Properties props = new Properties();
FileInputStream fis = new FileInputStream(marybase + configFile);
props.load(fis);
fis.close();
Map maryBaseReplacer = new HashMap();
maryBaseReplacer.put("jar:", marybase);
initHMMData(new PropertiesAccessor(props, false, maryBaseReplacer), voiceName);
}
public void initHMMData(String voiceName) throws IOException, MaryConfigurationException {
initHMMData(MaryConfig.getVoiceConfig(voiceName).getPropertiesAccessor(true), voiceName);
}
/**
* Reads from configuration file tree and pdf data for duration and f0 this method is used by HMMModel
*
* @param voiceName
* voiceName
* @throws IOException
* IOException
* @throws MaryConfigurationException
* MaryConfigurationException
*/
public void initHMMDataForHMMModel(String voiceName) throws IOException, MaryConfigurationException {
PropertiesAccessor p = MaryConfig.getVoiceConfig(voiceName).getPropertiesAccessor(true);
String prefix = "voice." + voiceName;
treeDurStream = p.getStream(prefix + ".Ftd");
pdfDurStream = p.getStream(prefix + ".Fmd");
treeLf0Stream = p.getStream(prefix + ".Ftf");
pdfLf0Stream = p.getStream(prefix + ".Fmf");
useGV = p.getBoolean(prefix + ".useGV");
if (useGV) {
useContextDependentGV = p.getBoolean(prefix + ".useContextDependentGV", useContextDependentGV);
if (p.getProperty(prefix + ".gvMethod") != null) {
String sval = p.getProperty(prefix + ".gvMethod");
setGvMethod(sval);
}
maxLf0GvIter = p.getInteger(prefix + ".maxLf0GvIter", maxLf0GvIter);
gvWeightLf0 = p.getDouble(prefix + ".gvWeightLf0", gvWeightLf0);
pdfLf0GVStream = p.getStream(prefix + ".Fgvf");
maxLf0GvIter = p.getInteger(prefix + ".maxLf0GvIter", maxLf0GvIter);
}
/* Example context feature file in MARY format */
InputStream feaStream = p.getStream(prefix + ".FeaFile");
feaDef = FeatureUtils.readFeatureDefinition(feaStream);
/*
* trickyPhones file, if any. If aliases for tricky phones were used during the training of HMMs then these aliases are in
* this file, if no aliases were used then the string is empty. This file will be used during the loading of HMM trees, so
* aliases of tricky phone are aplied back.
*/
InputStream trickyPhonesStream = p.getStream(prefix + ".trickyPhonesFile");
trickyPhones = new PhoneTranslator(trickyPhonesStream);
/* Load TreeSet ts and ModelSet ms for current voice */
logger.info("Loading Tree Set in CARTs:");
cart.loadTreeSet(this, feaDef, trickyPhones);
logger.info("Loading GV Model Set:");
gv.loadGVModelSet(this, feaDef);
}
/**
* Initialisation for mixed excitation : it loads the filter taps, they are read from MixFilterFile specified in the
* configuration file.
*
* @param mixFiltersStream
* mixFiltersStream
* @throws IOException
* IOException
*/
public void readMixedExcitationFilters(InputStream mixFiltersStream) throws IOException {
String line;
// first read the taps and then divide the total amount equally among the number of filters
Vector taps = new Vector();
/* get the filter coefficients */
Scanner s = null;
int i, j;
try {
s = new Scanner(new BufferedReader(new InputStreamReader(mixFiltersStream, "UTF-8")));
s.useLocale(Locale.US);
logger.debug("reading mixed excitation filters");
while (s.hasNext("#")) { /* skip comment lines */
line = s.nextLine();
// System.out.println("comment: " + line );
}
while (s.hasNextDouble())
taps.add(s.nextDouble());
} finally {
if (s != null) {
s.close();
}
}
orderFilters = (int) (taps.size() / numFilters);
mixFilters = new double[numFilters][orderFilters];
int k = 0;
for (i = 0; i < numFilters; i++) {
for (j = 0; j < orderFilters; j++) {
mixFilters[i][j] = taps.get(k++);
// System.out.println("h["+i+"]["+j+"]="+h[i][j]);
}
}
logger.debug("initMixedExcitation: loaded filter taps");
logger.debug("initMixedExcitation: numFilters = " + numFilters + " orderFilters = " + orderFilters);
} /* method readMixedExcitationFiltersFile() */
/**
* return the set of FeatureTypes that are available in this HMMData object
*
* @return featureTypes
*/
public Set getFeatureSet() {
Set featureTypes = EnumSet.noneOf(FeatureType.class);
if (getPdfDurStream() != null)
featureTypes.add(FeatureType.DUR);
if (getPdfLf0Stream() != null)
featureTypes.add(FeatureType.LF0);
if (getPdfStrStream() != null)
featureTypes.add(FeatureType.STR);
if (getPdfMagStream() != null)
featureTypes.add(FeatureType.MAG);
if (getPdfMgcStream() != null)
featureTypes.add(FeatureType.MGC);
return featureTypes;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy