001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.formats.png;
018
019import java.awt.image.BufferedImage;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.OutputStream;
023import java.nio.charset.StandardCharsets;
024import java.util.List;
025import java.util.zip.Deflater;
026import java.util.zip.DeflaterOutputStream;
027
028import org.apache.commons.imaging.ImagingException;
029import org.apache.commons.imaging.PixelDensity;
030import org.apache.commons.imaging.common.Allocator;
031import org.apache.commons.imaging.internal.Debug;
032import org.apache.commons.imaging.palette.Palette;
033import org.apache.commons.imaging.palette.PaletteFactory;
034
035public class PngWriter {
036
037    /*
038     * 1. IHDR: image header, which is the first chunk in a PNG data stream. 2. PLTE: palette table associated with indexed PNG images. 3. IDAT: image data
039     * chunks. 4. IEND: image trailer, which is the last chunk in a PNG data stream.
040     *
041     * The remaining 14 chunk types are termed ancillary chunk types, which encoders may generate and decoders may interpret.
042     *
043     * 1. Transparency information: tRNS (see 11.3.2: Transparency information). 2. Color space information: cHRM, gAMA, iCCP, sBIT, sRGB (see 11.3.3: Color
044     * space information). 3. Textual information: iTXt, tEXt, zTXt (see 11.3.4: Textual information). 4. Miscellaneous information: bKGD, hIST, pHYs, sPLT (see
045     * 11.3.5: Miscellaneous information). 5. Time information: tIME (see 11.3.6: Time stamp information).
046     */
047
048    private static final class ImageHeader {
049        public final int width;
050        public final int height;
051        public final byte bitDepth;
052        public final PngColorType pngColorType;
053        public final byte compressionMethod;
054        public final byte filterMethod;
055        public final InterlaceMethod interlaceMethod;
056
057        ImageHeader(final int width, final int height, final byte bitDepth, final PngColorType pngColorType, final byte compressionMethod,
058                final byte filterMethod, final InterlaceMethod interlaceMethod) {
059            this.width = width;
060            this.height = height;
061            this.bitDepth = bitDepth;
062            this.pngColorType = pngColorType;
063            this.compressionMethod = compressionMethod;
064            this.filterMethod = filterMethod;
065            this.interlaceMethod = interlaceMethod;
066        }
067
068    }
069
070    private byte[] deflate(final byte[] bytes) throws IOException {
071        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
072            try (DeflaterOutputStream dos = new DeflaterOutputStream(baos)) {
073                dos.write(bytes);
074                // dos.flush() doesn't work - we must close it before baos.toByteArray()
075            }
076            return baos.toByteArray();
077        }
078    }
079
080    private byte getBitDepth(final PngColorType pngColorType, final PngImagingParameters params) {
081        final byte depth = params.getBitDepth();
082
083        return pngColorType.isBitDepthAllowed(depth) ? depth : PngImagingParameters.DEFAULT_BIT_DEPTH;
084    }
085
086    private boolean isValidISO_8859_1(final String s) {
087        final String roundtrip = new String(s.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.ISO_8859_1);
088        return s.equals(roundtrip);
089    }
090
091    private void writeChunk(final OutputStream os, final ChunkType chunkType, final byte[] data) throws IOException {
092        final int dataLength = data == null ? 0 : data.length;
093        writeInt(os, dataLength);
094        os.write(chunkType.array);
095        if (data != null) {
096            os.write(data);
097        }
098
099        final PngCrc pngCrc = new PngCrc();
100
101        final long crc1 = pngCrc.startPartialCrc(chunkType.array, chunkType.array.length);
102        final long crc2 = data == null ? crc1 : pngCrc.continuePartialCrc(crc1, data, data.length);
103        final int crc = (int) pngCrc.finishPartialCrc(crc2);
104
105        writeInt(os, crc);
106    }
107
108    private void writeChunkIDAT(final OutputStream os, final byte[] bytes) throws IOException {
109        writeChunk(os, ChunkType.IDAT, bytes);
110    }
111
112    private void writeChunkIEND(final OutputStream os) throws IOException {
113        writeChunk(os, ChunkType.IEND, null);
114    }
115
116    private void writeChunkIHDR(final OutputStream os, final ImageHeader value) throws IOException {
117        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
118        writeInt(baos, value.width);
119        writeInt(baos, value.height);
120        baos.write(0xff & value.bitDepth);
121        baos.write(0xff & value.pngColorType.getValue());
122        baos.write(0xff & value.compressionMethod);
123        baos.write(0xff & value.filterMethod);
124        baos.write(0xff & value.interlaceMethod.ordinal());
125
126        writeChunk(os, ChunkType.IHDR, baos.toByteArray());
127    }
128
129    private void writeChunkiTXt(final OutputStream os, final AbstractPngText.Itxt text) throws IOException, ImagingException {
130        if (!isValidISO_8859_1(text.keyword)) {
131            throw new ImagingException("PNG tEXt chunk keyword is not ISO-8859-1: " + text.keyword);
132        }
133        if (!isValidISO_8859_1(text.languageTag)) {
134            throw new ImagingException("PNG tEXt chunk language tag is not ISO-8859-1: " + text.languageTag);
135        }
136
137        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
138
139        // keyword
140        baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
141        baos.write(0);
142
143        baos.write(1); // compressed flag, true
144        baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); // compression method
145
146        // language tag
147        baos.write(text.languageTag.getBytes(StandardCharsets.ISO_8859_1));
148        baos.write(0);
149
150        // translated keyword
151        baos.write(text.translatedKeyword.getBytes(StandardCharsets.UTF_8));
152        baos.write(0);
153
154        baos.write(deflate(text.text.getBytes(StandardCharsets.UTF_8)));
155
156        writeChunk(os, ChunkType.iTXt, baos.toByteArray());
157    }
158
159    private void writeChunkPHYS(final OutputStream os, final int xPPU, final int yPPU, final byte units) throws IOException {
160        final byte[] bytes = new byte[9];
161        bytes[0] = (byte) (0xff & xPPU >> 24);
162        bytes[1] = (byte) (0xff & xPPU >> 16);
163        bytes[2] = (byte) (0xff & xPPU >> 8);
164        bytes[3] = (byte) (0xff & xPPU >> 0);
165        bytes[4] = (byte) (0xff & yPPU >> 24);
166        bytes[5] = (byte) (0xff & yPPU >> 16);
167        bytes[6] = (byte) (0xff & yPPU >> 8);
168        bytes[7] = (byte) (0xff & yPPU >> 0);
169        bytes[8] = units;
170        writeChunk(os, ChunkType.pHYs, bytes);
171    }
172
173    private void writeChunkPLTE(final OutputStream os, final Palette palette) throws IOException {
174        final int length = palette.length();
175        final byte[] bytes = Allocator.byteArray(length * 3);
176
177        // Debug.debug("length", length);
178        for (int i = 0; i < length; i++) {
179            final int rgb = palette.getEntry(i);
180            final int index = i * 3;
181            // Debug.debug("index", index);
182            bytes[index + 0] = (byte) (0xff & rgb >> 16);
183            bytes[index + 1] = (byte) (0xff & rgb >> 8);
184            bytes[index + 2] = (byte) (0xff & rgb >> 0);
185        }
186
187        writeChunk(os, ChunkType.PLTE, bytes);
188    }
189
190    private void writeChunkSCAL(final OutputStream os, final double xUPP, final double yUPP, final byte units) throws IOException {
191        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
192
193        // unit specifier
194        baos.write(units);
195
196        // units per pixel, x-axis
197        baos.write(String.valueOf(xUPP).getBytes(StandardCharsets.ISO_8859_1));
198        baos.write(0);
199
200        baos.write(String.valueOf(yUPP).getBytes(StandardCharsets.ISO_8859_1));
201
202        writeChunk(os, ChunkType.sCAL, baos.toByteArray());
203    }
204
205    private void writeChunktEXt(final OutputStream os, final AbstractPngText.Text text) throws IOException, ImagingException {
206        if (!isValidISO_8859_1(text.keyword)) {
207            throw new ImagingException("PNG tEXt chunk keyword is not ISO-8859-1: " + text.keyword);
208        }
209        if (!isValidISO_8859_1(text.text)) {
210            throw new ImagingException("PNG tEXt chunk text is not ISO-8859-1: " + text.text);
211        }
212
213        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
214
215        // keyword
216        baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
217        baos.write(0);
218
219        // text
220        baos.write(text.text.getBytes(StandardCharsets.ISO_8859_1));
221
222        writeChunk(os, ChunkType.tEXt, baos.toByteArray());
223    }
224
225    private void writeChunkTRNS(final OutputStream os, final Palette palette) throws IOException {
226        final byte[] bytes = Allocator.byteArray(palette.length());
227
228        for (int i = 0; i < bytes.length; i++) {
229            bytes[i] = (byte) (0xff & palette.getEntry(i) >> 24);
230        }
231
232        writeChunk(os, ChunkType.tRNS, bytes);
233    }
234
235    private void writeChunkXmpiTXt(final OutputStream os, final String xmpXml) throws IOException {
236
237        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
238
239        // keyword
240        baos.write(PngConstants.XMP_KEYWORD.getBytes(StandardCharsets.ISO_8859_1));
241        baos.write(0);
242
243        baos.write(1); // compressed flag, true
244        baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE); // compression method
245
246        baos.write(0); // language tag (ignore). TODO
247
248        // translated keyword
249        baos.write(PngConstants.XMP_KEYWORD.getBytes(StandardCharsets.UTF_8));
250        baos.write(0);
251
252        baos.write(deflate(xmpXml.getBytes(StandardCharsets.UTF_8)));
253
254        writeChunk(os, ChunkType.iTXt, baos.toByteArray());
255    }
256
257    private void writeChunkzTXt(final OutputStream os, final AbstractPngText.Ztxt text) throws IOException, ImagingException {
258        if (!isValidISO_8859_1(text.keyword)) {
259            throw new ImagingException("PNG zTXt chunk keyword is not ISO-8859-1: " + text.keyword);
260        }
261        if (!isValidISO_8859_1(text.text)) {
262            throw new ImagingException("PNG zTXt chunk text is not ISO-8859-1: " + text.text);
263        }
264
265        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
266
267        // keyword
268        baos.write(text.keyword.getBytes(StandardCharsets.ISO_8859_1));
269        baos.write(0);
270
271        // compression method
272        baos.write(PngConstants.COMPRESSION_DEFLATE_INFLATE);
273
274        // text
275        baos.write(deflate(text.text.getBytes(StandardCharsets.ISO_8859_1)));
276
277        writeChunk(os, ChunkType.zTXt, baos.toByteArray());
278    }
279
280    /*
281     * between two chunk types indicates alternatives. Table 5.3 - Chunk ordering rules Critical chunks (shall appear in this order, except PLTE is optional)
282     * Chunk name Multiple allowed Ordering constraints IHDR No Shall be first PLTE No Before first IDAT IDAT Yes Multiple IDAT chunks shall be consecutive IEND
283     * No Shall be last Ancillary chunks (need not appear in this order) Chunk name Multiple allowed Ordering constraints cHRM No Before PLTE and IDAT gAMA No
284     * Before PLTE and IDAT iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present. sBIT No Before PLTE and IDAT sRGB
285     * No Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present. bKGD No After PLTE; before IDAT hIST No After PLTE; before
286     * IDAT tRNS No After PLTE; before IDAT pHYs No Before IDAT sCAL No Before IDAT sPLT Yes Before IDAT tIME No None iTXt Yes None tEXt Yes None zTXt Yes None
287     */
288
289    /**
290     * Writes an image to an output stream.
291     *
292     * @param src            The image to write.
293     * @param os             The output stream to write to.
294     * @param params         The parameters to use (can be {@code NULL} to use the default {@link PngImagingParameters}).
295     * @param paletteFactory The palette factory to use (can be {@code NULL} to use the default {@link PaletteFactory}).
296     * @throws ImagingException When errors are detected.
297     * @throws IOException      When IO problems occur.
298     */
299    public void writeImage(final BufferedImage src, final OutputStream os, PngImagingParameters params, PaletteFactory paletteFactory)
300            throws ImagingException, IOException {
301        if (params == null) {
302            params = new PngImagingParameters();
303        }
304        if (paletteFactory == null) {
305            paletteFactory = new PaletteFactory();
306        }
307        final int compressionLevel = Deflater.DEFAULT_COMPRESSION;
308
309        final int width = src.getWidth();
310        final int height = src.getHeight();
311
312        final boolean hasAlpha = paletteFactory.hasTransparency(src);
313        Debug.debug("hasAlpha: " + hasAlpha);
314        // int transparency = paletteFactory.getTransparency(src);
315
316        boolean isGrayscale = paletteFactory.isGrayscale(src);
317        Debug.debug("isGrayscale: " + isGrayscale);
318
319        PngColorType pngColorType;
320        {
321            final boolean forceIndexedColor = params.isForceIndexedColor();
322            final boolean forceTrueColor = params.isForceTrueColor();
323
324            if (forceIndexedColor && forceTrueColor) {
325                throw new ImagingException("Params: Cannot force both indexed and true color modes");
326            }
327            if (forceIndexedColor) {
328                pngColorType = PngColorType.INDEXED_COLOR;
329            } else if (forceTrueColor) {
330                pngColorType = hasAlpha ? PngColorType.TRUE_COLOR_WITH_ALPHA : PngColorType.TRUE_COLOR;
331                isGrayscale = false;
332            } else {
333                pngColorType = PngColorType.getColorType(hasAlpha, isGrayscale);
334            }
335            Debug.debug("colorType: " + pngColorType);
336        }
337
338        final byte bitDepth = getBitDepth(pngColorType, params);
339        Debug.debug("bitDepth: " + bitDepth);
340
341        int sampleDepth;
342        if (pngColorType == PngColorType.INDEXED_COLOR) {
343            sampleDepth = 8;
344        } else {
345            sampleDepth = bitDepth;
346        }
347        Debug.debug("sampleDepth: " + sampleDepth);
348
349        {
350            PngConstants.PNG_SIGNATURE.writeTo(os);
351        }
352        {
353            // IHDR must be first
354
355            final byte compressionMethod = PngConstants.COMPRESSION_TYPE_INFLATE_DEFLATE;
356            final byte filterMethod = PngConstants.FILTER_METHOD_ADAPTIVE;
357            final InterlaceMethod interlaceMethod = InterlaceMethod.NONE;
358
359            final ImageHeader imageHeader = new ImageHeader(width, height, bitDepth, pngColorType, compressionMethod, filterMethod, interlaceMethod);
360
361            writeChunkIHDR(os, imageHeader);
362        }
363
364        // {
365        // sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the
366        // iCCP chunk should not be present.
367
368        // charles
369        // }
370
371        Palette palette = null;
372        if (pngColorType == PngColorType.INDEXED_COLOR) {
373            // PLTE No Before first IDAT
374
375            final int maxColors = 256;
376
377            if (hasAlpha) {
378                palette = paletteFactory.makeQuantizedRgbaPalette(src, hasAlpha, maxColors);
379                writeChunkPLTE(os, palette);
380                writeChunkTRNS(os, palette);
381            } else {
382                palette = paletteFactory.makeQuantizedRgbPalette(src, maxColors);
383                writeChunkPLTE(os, palette);
384            }
385        }
386
387        final Object pixelDensityObj = params.getPixelDensity();
388        if (pixelDensityObj != null) {
389            final PixelDensity pixelDensity = (PixelDensity) pixelDensityObj;
390            if (pixelDensity.isUnitless()) {
391                writeChunkPHYS(os, (int) Math.round(pixelDensity.getRawHorizontalDensity()), (int) Math.round(pixelDensity.getRawVerticalDensity()), (byte) 0);
392            } else {
393                writeChunkPHYS(os, (int) Math.round(pixelDensity.horizontalDensityMetres()), (int) Math.round(pixelDensity.verticalDensityMetres()), (byte) 1);
394            }
395        }
396
397        final PhysicalScale physicalScale = params.getPhysicalScale();
398        if (physicalScale != null) {
399            writeChunkSCAL(os, physicalScale.getHorizontalUnitsPerPixel(), physicalScale.getVerticalUnitsPerPixel(),
400                    physicalScale.isInMeters() ? (byte) 1 : (byte) 2);
401        }
402
403        final String xmpXml = params.getXmpXml();
404        if (xmpXml != null) {
405            writeChunkXmpiTXt(os, xmpXml);
406        }
407
408        final List<? extends AbstractPngText> outputTexts = params.getTextChunks();
409        if (outputTexts != null) {
410            for (final AbstractPngText text : outputTexts) {
411                if (text instanceof AbstractPngText.Text) {
412                    writeChunktEXt(os, (AbstractPngText.Text) text);
413                } else if (text instanceof AbstractPngText.Ztxt) {
414                    writeChunkzTXt(os, (AbstractPngText.Ztxt) text);
415                } else if (text instanceof AbstractPngText.Itxt) {
416                    writeChunkiTXt(os, (AbstractPngText.Itxt) text);
417                } else {
418                    throw new ImagingException("Unknown text to embed in PNG: " + text);
419                }
420            }
421        }
422
423        {
424            // Debug.debug("writing IDAT");
425
426            // IDAT Yes Multiple IDAT chunks shall be consecutive
427
428            // 28 March 2022. At this time, we only apply the predictor
429            // for non-grayscale, true-color images. This choice is made
430            // out of caution and is not necessarily required by the PNG
431            // spec. We may broaden the use of predictors in future versions.
432            final boolean usePredictor = params.isPredictorEnabled() && !isGrayscale && palette == null;
433
434            byte[] uncompressed;
435            if (!usePredictor) {
436                final ByteArrayOutputStream baos = new ByteArrayOutputStream();
437
438                final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
439
440                final int[] row = Allocator.intArray(width);
441                for (int y = 0; y < height; y++) {
442                    // Debug.debug("y", y + "/" + height);
443                    src.getRGB(0, y, width, 1, row, 0, width);
444
445                    baos.write(FilterType.NONE.ordinal());
446                    for (int x = 0; x < width; x++) {
447                        final int argb = row[x];
448
449                        if (palette != null) {
450                            final int index = palette.getPaletteIndex(argb);
451                            baos.write(0xff & index);
452                        } else {
453                            final int alpha = 0xff & argb >> 24;
454                            final int red = 0xff & argb >> 16;
455                            final int green = 0xff & argb >> 8;
456                            final int blue = 0xff & argb >> 0;
457
458                            if (isGrayscale) {
459                                final int gray = (red + green + blue) / 3;
460                                // if (y == 0)
461                                // {
462                                // Debug.debug("gray: " + x + ", " + y +
463                                // " argb: 0x"
464                                // + Integer.toHexString(argb) + " gray: 0x"
465                                // + Integer.toHexString(gray));
466                                // // Debug.debug(x + ", " + y + " gray", gray);
467                                // // Debug.debug(x + ", " + y + " gray", gray);
468                                // Debug.debug(x + ", " + y + " gray", gray +
469                                // " " + Integer.toHexString(gray));
470                                // Debug.debug();
471                                // }
472                                baos.write(gray);
473                            } else {
474                                baos.write(red);
475                                baos.write(green);
476                                baos.write(blue);
477                            }
478                            if (useAlpha) {
479                                baos.write(alpha);
480                            }
481                        }
482                    }
483                }
484                uncompressed = baos.toByteArray();
485            } else {
486                final ByteArrayOutputStream baos = new ByteArrayOutputStream();
487
488                final boolean useAlpha = pngColorType == PngColorType.GREYSCALE_WITH_ALPHA || pngColorType == PngColorType.TRUE_COLOR_WITH_ALPHA;
489
490                final int[] row = Allocator.intArray(width);
491                for (int y = 0; y < height; y++) {
492                    // Debug.debug("y", y + "/" + height);
493                    src.getRGB(0, y, width, 1, row, 0, width);
494
495                    int priorA = 0;
496                    int priorR = 0;
497                    int priorG = 0;
498                    int priorB = 0;
499                    baos.write(FilterType.SUB.ordinal());
500                    for (int x = 0; x < width; x++) {
501                        final int argb = row[x];
502                        final int alpha = 0xff & argb >> 24;
503                        final int red = 0xff & argb >> 16;
504                        final int green = 0xff & argb >> 8;
505                        final int blue = 0xff & argb;
506
507                        baos.write(red - priorR);
508                        baos.write(green - priorG);
509                        baos.write(blue - priorB);
510                        priorR = red;
511                        priorG = green;
512                        priorB = blue;
513
514                        if (useAlpha) {
515                            baos.write(alpha - priorA);
516                            priorA = alpha;
517                        }
518                    }
519                }
520                uncompressed = baos.toByteArray();
521            }
522
523            // Debug.debug("uncompressed", uncompressed.length);
524
525            final ByteArrayOutputStream baos = new ByteArrayOutputStream();
526            final int chunkSize = 256 * 1024;
527            final Deflater deflater = new Deflater(compressionLevel);
528            final DeflaterOutputStream dos = new DeflaterOutputStream(baos, deflater, chunkSize);
529
530            for (int index = 0; index < uncompressed.length; index += chunkSize) {
531                final int end = Math.min(uncompressed.length, index + chunkSize);
532                final int length = end - index;
533
534                dos.write(uncompressed, index, length);
535                dos.flush();
536                baos.flush();
537
538                final byte[] compressed = baos.toByteArray();
539                baos.reset();
540                if (compressed.length > 0) {
541                    // Debug.debug("compressed", compressed.length);
542                    writeChunkIDAT(os, compressed);
543                }
544
545            }
546            {
547                dos.finish();
548                final byte[] compressed = baos.toByteArray();
549                if (compressed.length > 0) {
550                    // Debug.debug("compressed final", compressed.length);
551                    writeChunkIDAT(os, compressed);
552                }
553            }
554        }
555
556        {
557            // IEND No Shall be last
558
559            writeChunkIEND(os);
560        }
561
562        /*
563         * Ancillary chunks (need not appear in this order) Chunk name Multiple allowed Ordering constraints cHRM No Before PLTE and IDAT gAMA No Before PLTE
564         * and IDAT iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present. sBIT No Before PLTE and IDAT sRGB No
565         * Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present. bKGD No After PLTE; before IDAT hIST No After PLTE; before
566         * IDAT tRNS No After PLTE; before IDAT pHYs No Before IDAT sCAL No Before IDAT sPLT Yes Before IDAT tIME No None iTXt Yes None tEXt Yes None zTXt Yes
567         * None
568         */
569
570        os.close();
571    } // todo: filter types
572      // proper color types
573      // srgb, etc.
574
575    private void writeInt(final OutputStream os, final int value) throws IOException {
576        os.write(0xff & value >> 24);
577        os.write(0xff & value >> 16);
578        os.write(0xff & value >> 8);
579        os.write(0xff & value >> 0);
580    }
581}