aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/dumpkey/Android.mk (renamed from tools/ota/Android.mk)21
-rw-r--r--tools/dumpkey/DumpPublicKey.java270
-rw-r--r--tools/dumpkey/DumpPublicKey.mf1
-rw-r--r--tools/ota/add-property-tag.c141
-rw-r--r--tools/ota/check-lost+found.c145
-rw-r--r--tools/ota/convert-to-bmp.py79
6 files changed, 276 insertions, 381 deletions
diff --git a/tools/ota/Android.mk b/tools/dumpkey/Android.mk
index 142c3b25..31549146 100644
--- a/tools/ota/Android.mk
+++ b/tools/dumpkey/Android.mk
@@ -15,19 +15,8 @@
15LOCAL_PATH := $(call my-dir) 15LOCAL_PATH := $(call my-dir)
16 16
17include $(CLEAR_VARS) 17include $(CLEAR_VARS)
18LOCAL_FORCE_STATIC_EXECUTABLE := true 18LOCAL_MODULE := dumpkey
19LOCAL_MODULE := add-property-tag 19LOCAL_SRC_FILES := DumpPublicKey.java
20LOCAL_MODULE_PATH := $(TARGET_OUT_OPTIONAL_EXECUTABLES) 20LOCAL_JAR_MANIFEST := DumpPublicKey.mf
21LOCAL_MODULE_TAGS := debug 21LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host
22LOCAL_SRC_FILES := add-property-tag.c 22include $(BUILD_HOST_JAVA_LIBRARY)
23LOCAL_STATIC_LIBRARIES := libc
24include $(BUILD_EXECUTABLE)
25
26include $(CLEAR_VARS)
27LOCAL_FORCE_STATIC_EXECUTABLE := true
28LOCAL_MODULE := check-lost+found
29LOCAL_MODULE_PATH := $(TARGET_OUT_OPTIONAL_EXECUTABLES)
30LOCAL_MODULE_TAGS := debug
31LOCAL_SRC_FILES := check-lost+found.c
32LOCAL_STATIC_LIBRARIES := libcutils libc
33include $(BUILD_EXECUTABLE)
diff --git a/tools/dumpkey/DumpPublicKey.java b/tools/dumpkey/DumpPublicKey.java
new file mode 100644
index 00000000..3eb13984
--- /dev/null
+++ b/tools/dumpkey/DumpPublicKey.java
@@ -0,0 +1,270 @@
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dumpkey;
18
19import org.bouncycastle.jce.provider.BouncyCastleProvider;
20
21import java.io.FileInputStream;
22import java.math.BigInteger;
23import java.security.cert.CertificateFactory;
24import java.security.cert.X509Certificate;
25import java.security.KeyStore;
26import java.security.Key;
27import java.security.PublicKey;
28import java.security.Security;
29import java.security.interfaces.ECPublicKey;
30import java.security.interfaces.RSAPublicKey;
31import java.security.spec.ECPoint;
32
33/**
34 * Command line tool to extract RSA public keys from X.509 certificates
35 * and output source code with data initializers for the keys.
36 * @hide
37 */
38class DumpPublicKey {
39 /**
40 * @param key to perform sanity checks on
41 * @return version number of key. Supported versions are:
42 * 1: 2048-bit RSA key with e=3 and SHA-1 hash
43 * 2: 2048-bit RSA key with e=65537 and SHA-1 hash
44 * 3: 2048-bit RSA key with e=3 and SHA-256 hash
45 * 4: 2048-bit RSA key with e=65537 and SHA-256 hash
46 * @throws Exception if the key has the wrong size or public exponent
47 */
48 static int checkRSA(RSAPublicKey key, boolean useSHA256) throws Exception {
49 BigInteger pubexp = key.getPublicExponent();
50 BigInteger modulus = key.getModulus();
51 int version;
52
53 if (pubexp.equals(BigInteger.valueOf(3))) {
54 version = useSHA256 ? 3 : 1;
55 } else if (pubexp.equals(BigInteger.valueOf(65537))) {
56 version = useSHA256 ? 4 : 2;
57 } else {
58 throw new Exception("Public exponent should be 3 or 65537 but is " +
59 pubexp.toString(10) + ".");
60 }
61
62 if (modulus.bitLength() != 2048) {
63 throw new Exception("Modulus should be 2048 bits long but is " +
64 modulus.bitLength() + " bits.");
65 }
66
67 return version;
68 }
69
70 /**
71 * @param key to perform sanity checks on
72 * @return version number of key. Supported versions are:
73 * 5: 256-bit EC key with curve NIST P-256
74 * @throws Exception if the key has the wrong size or public exponent
75 */
76 static int checkEC(ECPublicKey key) throws Exception {
77 if (key.getParams().getCurve().getField().getFieldSize() != 256) {
78 throw new Exception("Curve must be NIST P-256");
79 }
80
81 return 5;
82 }
83
84 /**
85 * Perform sanity check on public key.
86 */
87 static int check(PublicKey key, boolean useSHA256) throws Exception {
88 if (key instanceof RSAPublicKey) {
89 return checkRSA((RSAPublicKey) key, useSHA256);
90 } else if (key instanceof ECPublicKey) {
91 if (!useSHA256) {
92 throw new Exception("Must use SHA-256 with EC keys!");
93 }
94 return checkEC((ECPublicKey) key);
95 } else {
96 throw new Exception("Unsupported key class: " + key.getClass().getName());
97 }
98 }
99
100 /**
101 * @param key to output
102 * @return a String representing this public key. If the key is a
103 * version 1 key, the string will be a C initializer; this is
104 * not true for newer key versions.
105 */
106 static String printRSA(RSAPublicKey key, boolean useSHA256) throws Exception {
107 int version = check(key, useSHA256);
108
109 BigInteger N = key.getModulus();
110
111 StringBuilder result = new StringBuilder();
112
113 int nwords = N.bitLength() / 32; // # of 32 bit integers in modulus
114
115 if (version > 1) {
116 result.append("v");
117 result.append(Integer.toString(version));
118 result.append(" ");
119 }
120
121 result.append("{");
122 result.append(nwords);
123
124 BigInteger B = BigInteger.valueOf(0x100000000L); // 2^32
125 BigInteger N0inv = B.subtract(N.modInverse(B)); // -1 / N[0] mod 2^32
126
127 result.append(",0x");
128 result.append(N0inv.toString(16));
129
130 BigInteger R = BigInteger.valueOf(2).pow(N.bitLength());
131 BigInteger RR = R.multiply(R).mod(N); // 2^4096 mod N
132
133 // Write out modulus as little endian array of integers.
134 result.append(",{");
135 for (int i = 0; i < nwords; ++i) {
136 long n = N.mod(B).longValue();
137 result.append(n);
138
139 if (i != nwords - 1) {
140 result.append(",");
141 }
142
143 N = N.divide(B);
144 }
145 result.append("}");
146
147 // Write R^2 as little endian array of integers.
148 result.append(",{");
149 for (int i = 0; i < nwords; ++i) {
150 long rr = RR.mod(B).longValue();
151 result.append(rr);
152
153 if (i != nwords - 1) {
154 result.append(",");
155 }
156
157 RR = RR.divide(B);
158 }
159 result.append("}");
160
161 result.append("}");
162 return result.toString();
163 }
164
165 /**
166 * @param key to output
167 * @return a String representing this public key. If the key is a
168 * version 1 key, the string will be a C initializer; this is
169 * not true for newer key versions.
170 */
171 static String printEC(ECPublicKey key) throws Exception {
172 int version = checkEC(key);
173
174 StringBuilder result = new StringBuilder();
175
176 result.append("v");
177 result.append(Integer.toString(version));
178 result.append(" ");
179
180 BigInteger X = key.getW().getAffineX();
181 BigInteger Y = key.getW().getAffineY();
182 int nbytes = key.getParams().getCurve().getField().getFieldSize() / 8; // # of 32 bit integers in X coordinate
183
184 result.append("{");
185 result.append(nbytes);
186
187 BigInteger B = BigInteger.valueOf(0x100L); // 2^8
188
189 // Write out Y coordinate as array of characters.
190 result.append(",{");
191 for (int i = 0; i < nbytes; ++i) {
192 long n = X.mod(B).longValue();
193 result.append(n);
194
195 if (i != nbytes - 1) {
196 result.append(",");
197 }
198
199 X = X.divide(B);
200 }
201 result.append("}");
202
203 // Write out Y coordinate as array of characters.
204 result.append(",{");
205 for (int i = 0; i < nbytes; ++i) {
206 long n = Y.mod(B).longValue();
207 result.append(n);
208
209 if (i != nbytes - 1) {
210 result.append(",");
211 }
212
213 Y = Y.divide(B);
214 }
215 result.append("}");
216
217 result.append("}");
218 return result.toString();
219 }
220
221 static String print(PublicKey key, boolean useSHA256) throws Exception {
222 if (key instanceof RSAPublicKey) {
223 return printRSA((RSAPublicKey) key, useSHA256);
224 } else if (key instanceof ECPublicKey) {
225 return printEC((ECPublicKey) key);
226 } else {
227 throw new Exception("Unsupported key class: " + key.getClass().getName());
228 }
229 }
230
231 public static void main(String[] args) {
232 if (args.length < 1) {
233 System.err.println("Usage: DumpPublicKey certfile ... > source.c");
234 System.exit(1);
235 }
236 Security.addProvider(new BouncyCastleProvider());
237 try {
238 for (int i = 0; i < args.length; i++) {
239 FileInputStream input = new FileInputStream(args[i]);
240 CertificateFactory cf = CertificateFactory.getInstance("X.509");
241 X509Certificate cert = (X509Certificate) cf.generateCertificate(input);
242
243 boolean useSHA256 = false;
244 String sigAlg = cert.getSigAlgName();
245 if ("SHA1withRSA".equals(sigAlg) || "MD5withRSA".equals(sigAlg)) {
246 // SignApk has historically accepted "MD5withRSA"
247 // certificates, but treated them as "SHA1withRSA"
248 // anyway. Continue to do so for backwards
249 // compatibility.
250 useSHA256 = false;
251 } else if ("SHA256withRSA".equals(sigAlg) || "SHA256withECDSA".equals(sigAlg)) {
252 useSHA256 = true;
253 } else {
254 System.err.println(args[i] + ": unsupported signature algorithm \"" +
255 sigAlg + "\"");
256 System.exit(1);
257 }
258
259 PublicKey key = cert.getPublicKey();
260 check(key, useSHA256);
261 System.out.print(print(key, useSHA256));
262 System.out.println(i < args.length - 1 ? "," : "");
263 }
264 } catch (Exception e) {
265 e.printStackTrace();
266 System.exit(1);
267 }
268 System.exit(0);
269 }
270}
diff --git a/tools/dumpkey/DumpPublicKey.mf b/tools/dumpkey/DumpPublicKey.mf
new file mode 100644
index 00000000..7bb3bc88
--- /dev/null
+++ b/tools/dumpkey/DumpPublicKey.mf
@@ -0,0 +1 @@
Main-Class: com.android.dumpkey.DumpPublicKey
diff --git a/tools/ota/add-property-tag.c b/tools/ota/add-property-tag.c
deleted file mode 100644
index aab30b2d..00000000
--- a/tools/ota/add-property-tag.c
+++ /dev/null
@@ -1,141 +0,0 @@
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include <ctype.h>
18#include <errno.h>
19#include <getopt.h>
20#include <limits.h>
21#include <stdio.h>
22#include <stdlib.h>
23#include <string.h>
24
25/*
26 * Append a tag to a property value in a .prop file if it isn't already there.
27 * Normally used to modify build properties to record incremental updates.
28 */
29
30// Return nonzero if the tag should be added to this line.
31int should_tag(const char *line, const char *propname) {
32 const char *prop = strstr(line, propname);
33 if (prop == NULL) return 0;
34
35 // Make sure this is actually the property name (not an accidental hit)
36 const char *ptr;
37 for (ptr = line; ptr < prop && isspace(*ptr); ++ptr) ;
38 if (ptr != prop) return 0; // Must be at the beginning of the line
39
40 for (ptr += strlen(propname); *ptr != '\0' && isspace(*ptr); ++ptr) ;
41 return (*ptr == '='); // Must be followed by a '='
42}
43
44// Remove existing tags from the line, return the following number (if any)
45int remove_tag(char *line, const char *tag) {
46 char *pos = strstr(line, tag);
47 if (pos == NULL) return 0;
48
49 char *end;
50 int num = strtoul(pos + strlen(tag), &end, 10);
51 strcpy(pos, end);
52 return num;
53}
54
55// Write line to output with the tag added, adding a number (if >0)
56void write_tagged(FILE *out, const char *line, const char *tag, int number) {
57 const char *end = line + strlen(line);
58 while (end > line && isspace(end[-1])) --end;
59 if (number > 0) {
60 fprintf(out, "%.*s%s%d%s", (int)(end - line), line, tag, number, end);
61 } else {
62 fprintf(out, "%.*s%s%s", (int)(end - line), line, tag, end);
63 }
64}
65
66int main(int argc, char **argv) {
67 const char *filename = "/system/build.prop";
68 const char *propname = "ro.build.fingerprint";
69 const char *tag = NULL;
70 int do_remove = 0, do_number = 0;
71
72 int opt;
73 while ((opt = getopt(argc, argv, "f:p:rn")) != -1) {
74 switch (opt) {
75 case 'f': filename = optarg; break;
76 case 'p': propname = optarg; break;
77 case 'r': do_remove = 1; break;
78 case 'n': do_number = 1; break;
79 case '?': return 2;
80 }
81 }
82
83 if (argc != optind + 1) {
84 fprintf(stderr,
85 "usage: add-property-tag [flags] tag-to-add\n"
86 "flags: -f /dir/file.prop (default /system/build.prop)\n"
87 " -p prop.name (default ro.build.fingerprint)\n"
88 " -r (if set, remove the tag rather than adding it)\n"
89 " -n (if set, add and increment a number after the tag)\n");
90 return 2;
91 }
92
93 tag = argv[optind];
94 FILE *input = fopen(filename, "r");
95 if (input == NULL) {
96 fprintf(stderr, "can't read %s: %s\n", filename, strerror(errno));
97 return 1;
98 }
99
100 char tmpname[PATH_MAX];
101 snprintf(tmpname, sizeof(tmpname), "%s.tmp", filename);
102 FILE *output = fopen(tmpname, "w");
103 if (output == NULL) {
104 fprintf(stderr, "can't write %s: %s\n", tmpname, strerror(errno));
105 return 1;
106 }
107
108 int found = 0;
109 char line[4096];
110 while (fgets(line, sizeof(line), input)) {
111 if (!should_tag(line, propname)) {
112 fputs(line, output); // Pass through unmodified
113 } else {
114 found = 1;
115 int number = remove_tag(line, tag);
116 if (do_remove) {
117 fputs(line, output); // Remove the tag but don't re-add it
118 } else {
119 write_tagged(output, line, tag, number + do_number);
120 }
121 }
122 }
123
124 fclose(input);
125 fclose(output);
126
127 if (!found) {
128 fprintf(stderr, "property %s not found in %s\n", propname, filename);
129 remove(tmpname);
130 return 1;
131 }
132
133 if (rename(tmpname, filename)) {
134 fprintf(stderr, "can't rename %s to %s: %s\n",
135 tmpname, filename, strerror(errno));
136 remove(tmpname);
137 return 1;
138 }
139
140 return 0;
141}
diff --git a/tools/ota/check-lost+found.c b/tools/ota/check-lost+found.c
deleted file mode 100644
index 8ce12d39..00000000
--- a/tools/ota/check-lost+found.c
+++ /dev/null
@@ -1,145 +0,0 @@
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include <dirent.h>
18#include <errno.h>
19#include <fcntl.h>
20#include <limits.h>
21#include <stdio.h>
22#include <stdlib.h>
23#include <string.h>
24#include <sys/klog.h>
25#include <sys/reboot.h>
26#include <sys/stat.h>
27#include <sys/types.h>
28#include <time.h>
29#include <unistd.h>
30
31#include "private/android_filesystem_config.h"
32
33// Sentinel file used to track whether we've forced a reboot
34static const char *kMarkerFile = "/data/misc/check-lost+found-rebooted-2";
35
36// Output file in tombstones directory (first 8K will be uploaded)
37static const char *kOutputDir = "/data/tombstones";
38static const char *kOutputFile = "/data/tombstones/check-lost+found-log";
39
40// Partitions to check
41static const char *kPartitions[] = { "/system", "/data", "/cache", NULL };
42
43/*
44 * 1. If /data/misc/forced-reboot is missing, touch it & force "unclean" boot.
45 * 2. Write a log entry with the number of files in lost+found directories.
46 */
47
48int main(int argc __attribute__((unused)), char **argv __attribute__((unused))) {
49 mkdir(kOutputDir, 0755);
50 chown(kOutputDir, AID_SYSTEM, AID_SYSTEM);
51 FILE *out = fopen(kOutputFile, "a");
52 if (out == NULL) {
53 fprintf(stderr, "Can't write %s: %s\n", kOutputFile, strerror(errno));
54 return 1;
55 }
56
57 // Note: only the first 8K of log will be uploaded, so be terse.
58 time_t start = time(NULL);
59 fprintf(out, "*** check-lost+found ***\nStarted: %s", ctime(&start));
60
61 struct stat st;
62 if (stat(kMarkerFile, &st)) {
63 // No reboot marker -- need to force an unclean reboot.
64 // But first, try to create the marker file. If that fails,
65 // skip the reboot, so we don't get caught in an infinite loop.
66
67 int fd = open(kMarkerFile, O_WRONLY|O_CREAT, 0444);
68 if (fd >= 0 && close(fd) == 0) {
69 fprintf(out, "Wrote %s, rebooting\n", kMarkerFile);
70 fflush(out);
71 sync(); // Make sure the marker file is committed to disk
72
73 // If possible, dirty each of these partitions before rebooting,
74 // to make sure the filesystem has to do a scan on mount.
75 int i;
76 for (i = 0; kPartitions[i] != NULL; ++i) {
77 char fn[PATH_MAX];
78 snprintf(fn, sizeof(fn), "%s/%s", kPartitions[i], "dirty");
79 fd = open(fn, O_WRONLY|O_CREAT, 0444);
80 if (fd >= 0) { // Don't sweat it if we can't write the file.
81 TEMP_FAILURE_RETRY(write(fd, fn, sizeof(fn))); // write, you know, some data
82 close(fd);
83 unlink(fn);
84 }
85 }
86
87 reboot(RB_AUTOBOOT); // reboot immediately, with dirty filesystems
88 fprintf(out, "Reboot failed?!\n");
89 exit(1);
90 } else {
91 fprintf(out, "Can't write %s: %s\n", kMarkerFile, strerror(errno));
92 }
93 } else {
94 fprintf(out, "Found %s\n", kMarkerFile);
95 }
96
97 int i;
98 for (i = 0; kPartitions[i] != NULL; ++i) {
99 char fn[PATH_MAX];
100 snprintf(fn, sizeof(fn), "%s/%s", kPartitions[i], "lost+found");
101 DIR *dir = opendir(fn);
102 if (dir == NULL) {
103 fprintf(out, "Can't open %s: %s\n", fn, strerror(errno));
104 } else {
105 int count = 0;
106 struct dirent *ent;
107 while ((ent = readdir(dir))) {
108 if (strcmp(ent->d_name, ".") && strcmp(ent->d_name, ".."))
109 ++count;
110 }
111 closedir(dir);
112 if (count > 0) {
113 fprintf(out, "OMGZ FOUND %d FILES IN %s\n", count, fn);
114 } else {
115 fprintf(out, "%s is clean\n", fn);
116 }
117 }
118 }
119
120 char dmesg[131073];
121 int len = klogctl(KLOG_READ_ALL, dmesg, sizeof(dmesg) - 1);
122 if (len < 0) {
123 fprintf(out, "Can't read kernel log: %s\n", strerror(errno));
124 } else { // To conserve space, only write lines with certain keywords
125 fprintf(out, "--- Kernel log ---\n");
126 dmesg[len] = '\0';
127 char *saveptr, *line;
128 int in_yaffs = 0;
129 for (line = strtok_r(dmesg, "\n", &saveptr); line != NULL;
130 line = strtok_r(NULL, "\n", &saveptr)) {
131 if (strstr(line, "yaffs: dev is")) in_yaffs = 1;
132
133 if (in_yaffs ||
134 strstr(line, "yaffs") ||
135 strstr(line, "mtd") ||
136 strstr(line, "msm_nand")) {
137 fprintf(out, "%s\n", line);
138 }
139
140 if (strstr(line, "yaffs_read_super: isCheckpointed")) in_yaffs = 0;
141 }
142 }
143
144 return 0;
145}
diff --git a/tools/ota/convert-to-bmp.py b/tools/ota/convert-to-bmp.py
deleted file mode 100644
index 446c09da..00000000
--- a/tools/ota/convert-to-bmp.py
+++ /dev/null
@@ -1,79 +0,0 @@
1#!/usr/bin/python2.4
2
3"""A simple script to convert asset images to BMP files, that supports
4RGBA image."""
5
6import struct
7import Image
8import sys
9
10infile = sys.argv[1]
11outfile = sys.argv[2]
12
13if not outfile.endswith(".bmp"):
14 print >> sys.stderr, "Warning: I'm expecting to write BMP files."
15
16im = Image.open(infile)
17if im.mode == 'RGB':
18 im.save(outfile)
19elif im.mode == 'RGBA':
20 # Python Imaging Library doesn't write RGBA BMP files, so we roll
21 # our own.
22
23 BMP_HEADER_FMT = ("<" # little-endian
24 "H" # signature
25 "L" # file size
26 "HH" # reserved (set to 0)
27 "L" # offset to start of bitmap data)
28 )
29
30 BITMAPINFO_HEADER_FMT= ("<" # little-endian
31 "L" # size of this struct
32 "L" # width
33 "L" # height
34 "H" # planes (set to 1)
35 "H" # bit count
36 "L" # compression (set to 0 for minui)
37 "L" # size of image data (0 if uncompressed)
38 "L" # x pixels per meter (1)
39 "L" # y pixels per meter (1)
40 "L" # colors used (0)
41 "L" # important colors (0)
42 )
43
44 fileheadersize = struct.calcsize(BMP_HEADER_FMT)
45 infoheadersize = struct.calcsize(BITMAPINFO_HEADER_FMT)
46
47 header = struct.pack(BMP_HEADER_FMT,
48 0x4d42, # "BM" in little-endian
49 (fileheadersize + infoheadersize +
50 im.size[0] * im.size[1] * 4),
51 0, 0,
52 fileheadersize + infoheadersize)
53
54 info = struct.pack(BITMAPINFO_HEADER_FMT,
55 infoheadersize,
56 im.size[0],
57 im.size[1],
58 1,
59 32,
60 0,
61 0,
62 1,
63 1,
64 0,
65 0)
66
67 f = open(outfile, "wb")
68 f.write(header)
69 f.write(info)
70 data = im.tostring()
71 for j in range(im.size[1]-1, -1, -1): # rows bottom-to-top
72 for i in range(j*im.size[0]*4, (j+1)*im.size[0]*4, 4):
73 f.write(data[i+2]) # B
74 f.write(data[i+1]) # G
75 f.write(data[i+0]) # R
76 f.write(data[i+3]) # A
77 f.close()
78else:
79 print >> sys.stderr, "Don't know how to handle image mode '%s'." % (im.mode,)