Encoder.java (zxing-zxing-3.4.1) | : | Encoder.java (zxing-zxing-3.5.0) | ||
---|---|---|---|---|
skipping to change at line 22 | skipping to change at line 22 | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | * See the License for the specific language governing permissions and | |||
* limitations under the License. | * limitations under the License. | |||
*/ | */ | |||
package com.google.zxing.qrcode.encoder; | package com.google.zxing.qrcode.encoder; | |||
import com.google.zxing.EncodeHintType; | import com.google.zxing.EncodeHintType; | |||
import com.google.zxing.WriterException; | import com.google.zxing.WriterException; | |||
import com.google.zxing.common.BitArray; | import com.google.zxing.common.BitArray; | |||
import com.google.zxing.common.StringUtils; | ||||
import com.google.zxing.common.CharacterSetECI; | import com.google.zxing.common.CharacterSetECI; | |||
import com.google.zxing.common.reedsolomon.GenericGF; | import com.google.zxing.common.reedsolomon.GenericGF; | |||
import com.google.zxing.common.reedsolomon.ReedSolomonEncoder; | import com.google.zxing.common.reedsolomon.ReedSolomonEncoder; | |||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; | import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; | |||
import com.google.zxing.qrcode.decoder.Mode; | import com.google.zxing.qrcode.decoder.Mode; | |||
import com.google.zxing.qrcode.decoder.Version; | import com.google.zxing.qrcode.decoder.Version; | |||
import java.io.UnsupportedEncodingException; | import java.nio.charset.Charset; | |||
import java.nio.charset.StandardCharsets; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | |||
import java.util.Collection; | import java.util.Collection; | |||
import java.util.Map; | import java.util.Map; | |||
/** | /** | |||
* @author satorux@google.com (Satoru Takabayashi) - creator | * @author satorux@google.com (Satoru Takabayashi) - creator | |||
* @author dswitkin@google.com (Daniel Switkin) - ported from C++ | * @author dswitkin@google.com (Daniel Switkin) - ported from C++ | |||
*/ | */ | |||
public final class Encoder { | public final class Encoder { | |||
// The original table is defined in the table 5 of JISX0510:2004 (p.19). | // The original table is defined in the table 5 of JISX0510:2004 (p.19). | |||
private static final int[] ALPHANUMERIC_TABLE = { | private static final int[] ALPHANUMERIC_TABLE = { | |||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0 x0f | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0 x0f | |||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0 x1f | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0 x1f | |||
36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0 x2f | 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0 x2f | |||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0 x3f | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0 x3f | |||
-1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0 x4f | -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0 x4f | |||
25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0 x5f | 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0 x5f | |||
}; | }; | |||
static final String DEFAULT_BYTE_MODE_ENCODING = "ISO-8859-1"; | static final Charset DEFAULT_BYTE_MODE_ENCODING = StandardCharsets.ISO_8859_1; | |||
private Encoder() { | private Encoder() { | |||
} | } | |||
// The mask penalty calculation is complicated. See Table 21 of JISX0510:2004 (p.45) for details. | // The mask penalty calculation is complicated. See Table 21 of JISX0510:2004 (p.45) for details. | |||
// Basically it applies four rules and summate all penalties. | // Basically it applies four rules and summate all penalties. | |||
private static int calculateMaskPenalty(ByteMatrix matrix) { | private static int calculateMaskPenalty(ByteMatrix matrix) { | |||
return MaskUtil.applyMaskPenaltyRule1(matrix) | return MaskUtil.applyMaskPenaltyRule1(matrix) | |||
+ MaskUtil.applyMaskPenaltyRule2(matrix) | + MaskUtil.applyMaskPenaltyRule2(matrix) | |||
+ MaskUtil.applyMaskPenaltyRule3(matrix) | + MaskUtil.applyMaskPenaltyRule3(matrix) | |||
skipping to change at line 79 | skipping to change at line 81 | |||
* or configuration | * or configuration | |||
*/ | */ | |||
public static QRCode encode(String content, ErrorCorrectionLevel ecLevel) thro ws WriterException { | public static QRCode encode(String content, ErrorCorrectionLevel ecLevel) thro ws WriterException { | |||
return encode(content, ecLevel, null); | return encode(content, ecLevel, null); | |||
} | } | |||
public static QRCode encode(String content, | public static QRCode encode(String content, | |||
ErrorCorrectionLevel ecLevel, | ErrorCorrectionLevel ecLevel, | |||
Map<EncodeHintType,?> hints) throws WriterExceptio n { | Map<EncodeHintType,?> hints) throws WriterExceptio n { | |||
Version version; | ||||
BitArray headerAndDataBits; | ||||
Mode mode; | ||||
boolean hasGS1FormatHint = hints != null && hints.containsKey(EncodeHintType | ||||
.GS1_FORMAT) && | ||||
Boolean.parseBoolean(hints.get(EncodeHintType.GS1_FORMAT).toString()); | ||||
boolean hasCompactionHint = hints != null && hints.containsKey(EncodeHintTyp | ||||
e.QR_COMPACT) && | ||||
Boolean.parseBoolean(hints.get(EncodeHintType.QR_COMPACT).toString()); | ||||
// Determine what character encoding has been specified by the caller, if an y | // Determine what character encoding has been specified by the caller, if an y | |||
String encoding = DEFAULT_BYTE_MODE_ENCODING; | Charset encoding = DEFAULT_BYTE_MODE_ENCODING; | |||
boolean hasEncodingHint = hints != null && hints.containsKey(EncodeHintType. CHARACTER_SET); | boolean hasEncodingHint = hints != null && hints.containsKey(EncodeHintType. CHARACTER_SET); | |||
if (hasEncodingHint) { | if (hasEncodingHint) { | |||
encoding = hints.get(EncodeHintType.CHARACTER_SET).toString(); | encoding = Charset.forName(hints.get(EncodeHintType.CHARACTER_SET).toStrin g()); | |||
} | } | |||
// Pick an encoding mode appropriate for the content. Note that this will no | if (hasCompactionHint) { | |||
t attempt to use | mode = Mode.BYTE; | |||
// multiple modes / segments even if that were more efficient. Twould be nic | ||||
e. | ||||
Mode mode = chooseMode(content, encoding); | ||||
// This will store the header information, like mode and | Charset priorityEncoding = encoding.equals(DEFAULT_BYTE_MODE_ENCODING) ? n | |||
// length, as well as "header" segments like an ECI segment. | ull : encoding; | |||
BitArray headerBits = new BitArray(); | MinimalEncoder.ResultList rn = MinimalEncoder.encode(content, null, priori | |||
tyEncoding, hasGS1FormatHint, ecLevel); | ||||
// Append ECI segment if applicable | headerAndDataBits = new BitArray(); | |||
if (mode == Mode.BYTE && hasEncodingHint) { | rn.getBits(headerAndDataBits); | |||
CharacterSetECI eci = CharacterSetECI.getCharacterSetECIByName(encoding); | version = rn.getVersion(); | |||
if (eci != null) { | ||||
appendECI(eci, headerBits); | ||||
} | ||||
} | ||||
// Append the FNC1 mode header for GS1 formatted data if applicable | } else { | |||
boolean hasGS1FormatHint = hints != null && hints.containsKey(EncodeHintType | ||||
.GS1_FORMAT); | ||||
if (hasGS1FormatHint && Boolean.parseBoolean(hints.get(EncodeHintType.GS1_FO | ||||
RMAT).toString())) { | ||||
// GS1 formatted codes are prefixed with a FNC1 in first position mode hea | ||||
der | ||||
appendModeInfo(Mode.FNC1_FIRST_POSITION, headerBits); | ||||
} | ||||
// (With ECI in place,) Write the mode marker | ||||
appendModeInfo(mode, headerBits); | ||||
// Collect data within the main segment, separately, to count its size if ne | // Pick an encoding mode appropriate for the content. Note that this will | |||
eded. Don't add it to | not attempt to use | |||
// main payload yet. | // multiple modes / segments even if that were more efficient. | |||
BitArray dataBits = new BitArray(); | mode = chooseMode(content, encoding); | |||
appendBytes(content, mode, dataBits, encoding); | ||||
// This will store the header information, like mode and | ||||
// length, as well as "header" segments like an ECI segment. | ||||
BitArray headerBits = new BitArray(); | ||||
// Append ECI segment if applicable | ||||
if (mode == Mode.BYTE && hasEncodingHint) { | ||||
CharacterSetECI eci = CharacterSetECI.getCharacterSetECI(encoding); | ||||
if (eci != null) { | ||||
appendECI(eci, headerBits); | ||||
} | ||||
} | ||||
Version version; | // Append the FNC1 mode header for GS1 formatted data if applicable | |||
if (hints != null && hints.containsKey(EncodeHintType.QR_VERSION)) { | if (hasGS1FormatHint) { | |||
int versionNumber = Integer.parseInt(hints.get(EncodeHintType.QR_VERSION). | // GS1 formatted codes are prefixed with a FNC1 in first position mode h | |||
toString()); | eader | |||
version = Version.getVersionForNumber(versionNumber); | appendModeInfo(Mode.FNC1_FIRST_POSITION, headerBits); | |||
int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, version); | } | |||
if (!willFit(bitsNeeded, version, ecLevel)) { | ||||
throw new WriterException("Data too big for requested version"); | // (With ECI in place,) Write the mode marker | |||
appendModeInfo(mode, headerBits); | ||||
// Collect data within the main segment, separately, to count its size if | ||||
needed. Don't add it to | ||||
// main payload yet. | ||||
BitArray dataBits = new BitArray(); | ||||
appendBytes(content, mode, dataBits, encoding); | ||||
if (hints != null && hints.containsKey(EncodeHintType.QR_VERSION)) { | ||||
int versionNumber = Integer.parseInt(hints.get(EncodeHintType.QR_VERSION | ||||
).toString()); | ||||
version = Version.getVersionForNumber(versionNumber); | ||||
int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, version | ||||
); | ||||
if (!willFit(bitsNeeded, version, ecLevel)) { | ||||
throw new WriterException("Data too big for requested version"); | ||||
} | ||||
} else { | ||||
version = recommendVersion(ecLevel, mode, headerBits, dataBits); | ||||
} | } | |||
} else { | ||||
version = recommendVersion(ecLevel, mode, headerBits, dataBits); | ||||
} | ||||
BitArray headerAndDataBits = new BitArray(); | headerAndDataBits = new BitArray(); | |||
headerAndDataBits.appendBitArray(headerBits); | headerAndDataBits.appendBitArray(headerBits); | |||
// Find "length" of main segment and write it | // Find "length" of main segment and write it | |||
int numLetters = mode == Mode.BYTE ? dataBits.getSizeInBytes() : content.len | int numLetters = mode == Mode.BYTE ? dataBits.getSizeInBytes() : content.l | |||
gth(); | ength(); | |||
appendLengthInfo(numLetters, version, mode, headerAndDataBits); | appendLengthInfo(numLetters, version, mode, headerAndDataBits); | |||
// Put data together into the overall payload | // Put data together into the overall payload | |||
headerAndDataBits.appendBitArray(dataBits); | headerAndDataBits.appendBitArray(dataBits); | |||
} | ||||
Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); | Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); | |||
int numDataBytes = version.getTotalCodewords() - ecBlocks.getTotalECCodeword s(); | int numDataBytes = version.getTotalCodewords() - ecBlocks.getTotalECCodeword s(); | |||
// Terminate the bits properly. | // Terminate the bits properly. | |||
terminateBits(numDataBytes, headerAndDataBits); | terminateBits(numDataBytes, headerAndDataBits); | |||
// Interleave data bits with error correction code. | // Interleave data bits with error correction code. | |||
BitArray finalBits = interleaveWithECBytes(headerAndDataBits, | BitArray finalBits = interleaveWithECBytes(headerAndDataBits, | |||
version.getTotalCodewords(), | version.getTotalCodewords(), | |||
skipping to change at line 224 | skipping to change at line 246 | |||
} | } | |||
public static Mode chooseMode(String content) { | public static Mode chooseMode(String content) { | |||
return chooseMode(content, null); | return chooseMode(content, null); | |||
} | } | |||
/** | /** | |||
* Choose the best mode by examining the content. Note that 'encoding' is used as a hint; | * Choose the best mode by examining the content. Note that 'encoding' is used as a hint; | |||
* if it is Shift_JIS, and the input is only double-byte Kanji, then we return {@link Mode#KANJI}. | * if it is Shift_JIS, and the input is only double-byte Kanji, then we return {@link Mode#KANJI}. | |||
*/ | */ | |||
private static Mode chooseMode(String content, String encoding) { | private static Mode chooseMode(String content, Charset encoding) { | |||
if ("Shift_JIS".equals(encoding) && isOnlyDoubleByteKanji(content)) { | if (StringUtils.SHIFT_JIS_CHARSET.equals(encoding) && isOnlyDoubleByteKanji( | |||
content)) { | ||||
// Choose Kanji mode if all input are double-byte characters | // Choose Kanji mode if all input are double-byte characters | |||
return Mode.KANJI; | return Mode.KANJI; | |||
} | } | |||
boolean hasNumeric = false; | boolean hasNumeric = false; | |||
boolean hasAlphanumeric = false; | boolean hasAlphanumeric = false; | |||
for (int i = 0; i < content.length(); ++i) { | for (int i = 0; i < content.length(); ++i) { | |||
char c = content.charAt(i); | char c = content.charAt(i); | |||
if (c >= '0' && c <= '9') { | if (c >= '0' && c <= '9') { | |||
hasNumeric = true; | hasNumeric = true; | |||
} else if (getAlphanumericCode(c) != -1) { | } else if (getAlphanumericCode(c) != -1) { | |||
skipping to change at line 250 | skipping to change at line 272 | |||
} | } | |||
if (hasAlphanumeric) { | if (hasAlphanumeric) { | |||
return Mode.ALPHANUMERIC; | return Mode.ALPHANUMERIC; | |||
} | } | |||
if (hasNumeric) { | if (hasNumeric) { | |||
return Mode.NUMERIC; | return Mode.NUMERIC; | |||
} | } | |||
return Mode.BYTE; | return Mode.BYTE; | |||
} | } | |||
private static boolean isOnlyDoubleByteKanji(String content) { | static boolean isOnlyDoubleByteKanji(String content) { | |||
byte[] bytes; | byte[] bytes = content.getBytes(StringUtils.SHIFT_JIS_CHARSET); | |||
try { | ||||
bytes = content.getBytes("Shift_JIS"); | ||||
} catch (UnsupportedEncodingException ignored) { | ||||
return false; | ||||
} | ||||
int length = bytes.length; | int length = bytes.length; | |||
if (length % 2 != 0) { | if (length % 2 != 0) { | |||
return false; | return false; | |||
} | } | |||
for (int i = 0; i < length; i += 2) { | for (int i = 0; i < length; i += 2) { | |||
int byte1 = bytes[i] & 0xFF; | int byte1 = bytes[i] & 0xFF; | |||
if ((byte1 < 0x81 || byte1 > 0x9F) && (byte1 < 0xE0 || byte1 > 0xEB)) { | if ((byte1 < 0x81 || byte1 > 0x9F) && (byte1 < 0xE0 || byte1 > 0xEB)) { | |||
return false; | return false; | |||
} | } | |||
} | } | |||
skipping to change at line 303 | skipping to change at line 320 | |||
return version; | return version; | |||
} | } | |||
} | } | |||
throw new WriterException("Data too big"); | throw new WriterException("Data too big"); | |||
} | } | |||
/** | /** | |||
* @return true if the number of input bits will fit in a code with the specif ied version and | * @return true if the number of input bits will fit in a code with the specif ied version and | |||
* error correction level. | * error correction level. | |||
*/ | */ | |||
private static boolean willFit(int numInputBits, Version version, ErrorCorrect | static boolean willFit(int numInputBits, Version version, ErrorCorrectionLevel | |||
ionLevel ecLevel) { | ecLevel) { | |||
// In the following comments, we use numbers of Version 7-H. | // In the following comments, we use numbers of Version 7-H. | |||
// numBytes = 196 | // numBytes = 196 | |||
int numBytes = version.getTotalCodewords(); | int numBytes = version.getTotalCodewords(); | |||
// getNumECBytes = 130 | // getNumECBytes = 130 | |||
Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); | Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); | |||
int numEcBytes = ecBlocks.getTotalECCodewords(); | int numEcBytes = ecBlocks.getTotalECCodewords(); | |||
// getNumDataBytes = 196 - 130 = 66 | // getNumDataBytes = 196 - 130 = 66 | |||
int numDataBytes = numBytes - numEcBytes; | int numDataBytes = numBytes - numEcBytes; | |||
int totalInputBytes = (numInputBits + 7) / 8; | int totalInputBytes = (numInputBits + 7) / 8; | |||
return numDataBytes >= totalInputBytes; | return numDataBytes >= totalInputBytes; | |||
} | } | |||
/** | /** | |||
* Terminate bits as described in 8.4.8 and 8.4.9 of JISX0510:2004 (p.24). | * Terminate bits as described in 8.4.8 and 8.4.9 of JISX0510:2004 (p.24). | |||
*/ | */ | |||
static void terminateBits(int numDataBytes, BitArray bits) throws WriterExcept ion { | static void terminateBits(int numDataBytes, BitArray bits) throws WriterExcept ion { | |||
int capacity = numDataBytes * 8; | int capacity = numDataBytes * 8; | |||
if (bits.getSize() > capacity) { | if (bits.getSize() > capacity) { | |||
throw new WriterException("data bits cannot fit in the QR Code" + bits.get Size() + " > " + | throw new WriterException("data bits cannot fit in the QR Code" + bits.get Size() + " > " + | |||
capacity); | capacity); | |||
} | } | |||
// Append Mode.TERMINATE if there is enough space (value is 0000) | ||||
for (int i = 0; i < 4 && bits.getSize() < capacity; ++i) { | for (int i = 0; i < 4 && bits.getSize() < capacity; ++i) { | |||
bits.appendBit(false); | bits.appendBit(false); | |||
} | } | |||
// Append termination bits. See 8.4.8 of JISX0510:2004 (p.24) for details. | // Append termination bits. See 8.4.8 of JISX0510:2004 (p.24) for details. | |||
// If the last byte isn't 8-bit aligned, we'll add padding bits. | // If the last byte isn't 8-bit aligned, we'll add padding bits. | |||
int numBitsInLastByte = bits.getSize() & 0x07; | int numBitsInLastByte = bits.getSize() & 0x07; | |||
if (numBitsInLastByte > 0) { | if (numBitsInLastByte > 0) { | |||
for (int i = numBitsInLastByte; i < 8; i++) { | for (int i = numBitsInLastByte; i < 8; i++) { | |||
bits.appendBit(false); | bits.appendBit(false); | |||
} | } | |||
skipping to change at line 514 | skipping to change at line 532 | |||
} | } | |||
bits.appendBits(numLetters, numBits); | bits.appendBits(numLetters, numBits); | |||
} | } | |||
/** | /** | |||
* Append "bytes" in "mode" mode (encoding) into "bits". On success, store the result in "bits". | * Append "bytes" in "mode" mode (encoding) into "bits". On success, store the result in "bits". | |||
*/ | */ | |||
static void appendBytes(String content, | static void appendBytes(String content, | |||
Mode mode, | Mode mode, | |||
BitArray bits, | BitArray bits, | |||
String encoding) throws WriterException { | Charset encoding) throws WriterException { | |||
switch (mode) { | switch (mode) { | |||
case NUMERIC: | case NUMERIC: | |||
appendNumericBytes(content, bits); | appendNumericBytes(content, bits); | |||
break; | break; | |||
case ALPHANUMERIC: | case ALPHANUMERIC: | |||
appendAlphanumericBytes(content, bits); | appendAlphanumericBytes(content, bits); | |||
break; | break; | |||
case BYTE: | case BYTE: | |||
append8BitBytes(content, bits, encoding); | append8BitBytes(content, bits, encoding); | |||
break; | break; | |||
skipping to change at line 581 | skipping to change at line 599 | |||
bits.appendBits(code1 * 45 + code2, 11); | bits.appendBits(code1 * 45 + code2, 11); | |||
i += 2; | i += 2; | |||
} else { | } else { | |||
// Encode one alphanumeric letter in six bits. | // Encode one alphanumeric letter in six bits. | |||
bits.appendBits(code1, 6); | bits.appendBits(code1, 6); | |||
i++; | i++; | |||
} | } | |||
} | } | |||
} | } | |||
static void append8BitBytes(String content, BitArray bits, String encoding) | static void append8BitBytes(String content, BitArray bits, Charset encoding) { | |||
throws WriterException { | byte[] bytes = content.getBytes(encoding); | |||
byte[] bytes; | ||||
try { | ||||
bytes = content.getBytes(encoding); | ||||
} catch (UnsupportedEncodingException uee) { | ||||
throw new WriterException(uee); | ||||
} | ||||
for (byte b : bytes) { | for (byte b : bytes) { | |||
bits.appendBits(b, 8); | bits.appendBits(b, 8); | |||
} | } | |||
} | } | |||
static void appendKanjiBytes(String content, BitArray bits) throws WriterExcep tion { | static void appendKanjiBytes(String content, BitArray bits) throws WriterExcep tion { | |||
byte[] bytes; | byte[] bytes = content.getBytes(StringUtils.SHIFT_JIS_CHARSET); | |||
try { | ||||
bytes = content.getBytes("Shift_JIS"); | ||||
} catch (UnsupportedEncodingException uee) { | ||||
throw new WriterException(uee); | ||||
} | ||||
if (bytes.length % 2 != 0) { | if (bytes.length % 2 != 0) { | |||
throw new WriterException("Kanji byte size not even"); | throw new WriterException("Kanji byte size not even"); | |||
} | } | |||
int maxI = bytes.length - 1; // bytes.length must be even | int maxI = bytes.length - 1; // bytes.length must be even | |||
for (int i = 0; i < maxI; i += 2) { | for (int i = 0; i < maxI; i += 2) { | |||
int byte1 = bytes[i] & 0xFF; | int byte1 = bytes[i] & 0xFF; | |||
int byte2 = bytes[i + 1] & 0xFF; | int byte2 = bytes[i + 1] & 0xFF; | |||
int code = (byte1 << 8) | byte2; | int code = (byte1 << 8) | byte2; | |||
int subtracted = -1; | int subtracted = -1; | |||
if (code >= 0x8140 && code <= 0x9ffc) { | if (code >= 0x8140 && code <= 0x9ffc) { | |||
End of changes. 21 change blocks. | ||||
91 lines changed or deleted | 101 lines changed or added |