mirror of https://github.com/mgba-emu/mgba.git
Merge branch 'master' (early part) into medusa
This commit is contained in:
commit
f7943aaa98
8
CHANGES
8
CHANGES
|
@ -38,10 +38,18 @@ Emulation fixes:
|
|||
- Core: Fix first event scheduling after loading savestate
|
||||
- GB Serialize: Fix switching speed modes when loading a state (fixes mgba.io/i/2097)
|
||||
- GBA Memory: Fix loading Thumb savestates when in ARM mode
|
||||
- GBA Video: Fix window start on modes 3-5 with mosaic (fixes mgba.io/i/1690)
|
||||
- GBA Video: Fix mode 3-5 overflow with mosaic (fixes mgba.io/i/1691)
|
||||
Other fixes:
|
||||
- GBA: Fix non-USA 1.0 FireRed misdetecting as a ROM hack (fixes mgba.io/i/2100)
|
||||
- GBA: Fix crash when ROM loading fails
|
||||
- GBA e-Reader: Fix bitmap short strip scanning
|
||||
- GBA Video: Fix mode 5 frame 1 caching (fixes mgba.io/i/2075)
|
||||
- GBA Video: Don't attempt to copy invalid registers when switching renderer
|
||||
Misc:
|
||||
- Core: Truncate preloading ROMs that slightly exceed max size (fixes mgba.io/i/2093)
|
||||
- GBA: Default-enable VBA bug compat for Ruby and Emerald ROM hacks
|
||||
- GBA Memory: Log GPIO writes on non-GPIO carts as Pak Hardware instead of Memory
|
||||
- Qt: Add ROM filename and size to bug reporter
|
||||
|
||||
0.9.0: (2021-03-28)
|
||||
|
|
|
@ -139,18 +139,18 @@ void mBitmapCacheCleanRow(struct mBitmapCache* cache, struct mBitmapCacheEntry*
|
|||
return;
|
||||
}
|
||||
|
||||
size_t offset = cache->bitsStart[cache->buffer] + y * mBitmapCacheSystemInfoGetWidth(cache->sysConfig);
|
||||
size_t offset = y * mBitmapCacheSystemInfoGetWidth(cache->sysConfig);
|
||||
void* vram;
|
||||
int bpe = mBitmapCacheSystemInfoGetEntryBPP(cache->sysConfig);
|
||||
uint32_t (*lookupEntry)(void*, uint32_t);
|
||||
switch (bpe) {
|
||||
case 3:
|
||||
lookupEntry = _lookupEntry8;
|
||||
vram = &cache->vram[offset];
|
||||
vram = &cache->vram[offset + cache->bitsStart[cache->buffer]];
|
||||
break;
|
||||
case 4:
|
||||
lookupEntry = _lookupEntry15;
|
||||
vram = &cache->vram[offset << 1];
|
||||
vram = &cache->vram[offset * 2 + cache->bitsStart[cache->buffer]];
|
||||
break;
|
||||
default:
|
||||
abort();
|
||||
|
|
|
@ -166,7 +166,13 @@ bool mCorePreloadVFCB(struct mCore* core, struct VFile* vf, void (cb)(size_t, si
|
|||
extern uint32_t* romBuffer;
|
||||
extern size_t romBufferSize;
|
||||
if (size > romBufferSize) {
|
||||
return false;
|
||||
if (size - romBufferSize < romBufferSize / 2) {
|
||||
// Some ROM hacks accidentally overflow the size a bit, but since those are broken
|
||||
// on hardware anyway we can just silently truncate them without issue.
|
||||
size = romBufferSize;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
vfm = VFileFromMemory(romBuffer, size);
|
||||
#else
|
||||
|
|
|
@ -370,6 +370,11 @@ void GBAHardwareEReaderScan(struct GBACartridgeHardware* hw, const void* data, s
|
|||
// Bitmap sizes
|
||||
case 5456:
|
||||
bitmap = true;
|
||||
blocks = 124;
|
||||
break;
|
||||
case 3520:
|
||||
bitmap = true;
|
||||
blocks = 80;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
@ -380,9 +385,9 @@ void GBAHardwareEReaderScan(struct GBACartridgeHardware* hw, const void* data, s
|
|||
if (bitmap) {
|
||||
size_t x;
|
||||
for (i = 0; i < 40; ++i) {
|
||||
const uint8_t* line = &cdata[(i + 2) * 124];
|
||||
const uint8_t* line = &cdata[(i + 2) * blocks];
|
||||
uint8_t* origin = &hw->eReaderDots[EREADER_DOTCODE_STRIDE * i + 200];
|
||||
for (x = 0; x < 124; ++x) {
|
||||
for (x = 0; x < blocks; ++x) {
|
||||
uint8_t byte = line[x];
|
||||
if (x == 123) {
|
||||
byte &= 0xE0;
|
||||
|
|
|
@ -912,7 +912,11 @@ void GBAStore16(struct ARMCore* cpu, uint32_t address, int16_t value, int* cycle
|
|||
}
|
||||
break;
|
||||
case REGION_CART0:
|
||||
if (memory->hw.devices != HW_NONE && IS_GPIO_REGISTER(address & 0xFFFFFE)) {
|
||||
if (IS_GPIO_REGISTER(address & 0xFFFFFE)) {
|
||||
if (memory->hw.devices == HW_NONE) {
|
||||
mLOG(GBA_HW, WARN, "Write to GPIO address %08X on cartridge without GPIO", address);
|
||||
break;
|
||||
}
|
||||
uint32_t reg = address & 0xFFFFFE;
|
||||
GBAHardwareGPIOWrite(&memory->hw, reg, value);
|
||||
break;
|
||||
|
|
|
@ -376,6 +376,14 @@ void GBAOverrideApplyDefaults(struct GBA* gba, const struct Configuration* overr
|
|||
memcpy(override.id, &cart->id, sizeof(override.id));
|
||||
|
||||
static const uint32_t pokemonTable[] = {
|
||||
// Emerald
|
||||
0x4881F3F8, // BPEJ
|
||||
0x8C4D3108, // BPES
|
||||
0x1F1C08FB, // BPEE
|
||||
0x34C9DF89, // BPED
|
||||
0xA3FDCCB1, // BPEF
|
||||
0xA0AEC80A, // BPEI
|
||||
|
||||
// FireRed
|
||||
0x1A81EEDF, // BPRD
|
||||
0x3B2056E9, // BPRJ
|
||||
|
@ -385,10 +393,17 @@ void GBAOverrideApplyDefaults(struct GBA* gba, const struct Configuration* overr
|
|||
0x9F08064E, // BPRS
|
||||
0xBB640DF7, // BPRJ rev 1
|
||||
0xDD88761C, // BPRE
|
||||
|
||||
// Ruby
|
||||
0x61641576, // AXVE rev 1
|
||||
0xAEAC73E6, // AXVE rev 2
|
||||
0xF0815EE7, // AXVE
|
||||
};
|
||||
|
||||
bool isPokemon = false;
|
||||
isPokemon = isPokemon || !strncmp("pokemon red version", &((const char*) gba->memory.rom)[0x108], 20);
|
||||
isPokemon = isPokemon || !strncmp("pokemon emerald version", &((const char*) gba->memory.rom)[0x108], 24);
|
||||
isPokemon = isPokemon || !strncmp("AXVE", &((const char*) gba->memory.rom)[0xAC], 4);
|
||||
bool isKnownPokemon = false;
|
||||
if (isPokemon) {
|
||||
size_t i;
|
||||
|
|
|
@ -8,16 +8,14 @@
|
|||
#include <mgba/core/interface.h>
|
||||
#include <mgba/internal/gba/gba.h>
|
||||
|
||||
#define BACKGROUND_BITMAP_ITERATE(W, H) \
|
||||
x += background->dx; \
|
||||
y += background->dy; \
|
||||
\
|
||||
if (x < 0 || y < 0 || (x >> 8) >= W || (y >> 8) >= H) { \
|
||||
continue; \
|
||||
} else { \
|
||||
localX = x; \
|
||||
localY = y; \
|
||||
}
|
||||
#define BACKGROUND_BITMAP_ITERATE(W, H) \
|
||||
x += background->dx; \
|
||||
y += background->dy; \
|
||||
if ((x < 0 || y < 0 || (x >> 8) >= W || (y >> 8) >= H) && !mosaicWait) { \
|
||||
continue; \
|
||||
} \
|
||||
localX = x; \
|
||||
localY = y;
|
||||
|
||||
#define MODE_2_COORD_OVERFLOW \
|
||||
localX = x & (sizeAdjusted - 1); \
|
||||
|
@ -65,12 +63,20 @@
|
|||
#define DRAW_BACKGROUND_MODE_2(BLEND, OBJWIN) \
|
||||
if (background->overflow) { \
|
||||
if (mosaicH > 1) { \
|
||||
localX &= sizeAdjusted - 1; \
|
||||
localY &= sizeAdjusted - 1; \
|
||||
MODE_2_NO_MOSAIC(); \
|
||||
MODE_2_LOOP(MODE_2_MOSAIC, MODE_2_COORD_OVERFLOW, BLEND, OBJWIN); \
|
||||
} else { \
|
||||
MODE_2_LOOP(MODE_2_NO_MOSAIC, MODE_2_COORD_OVERFLOW, BLEND, OBJWIN); \
|
||||
} \
|
||||
} else { \
|
||||
if (mosaicH > 1) { \
|
||||
if (!((x | y) & ~(sizeAdjusted - 1))) { \
|
||||
localX &= sizeAdjusted - 1; \
|
||||
localY &= sizeAdjusted - 1; \
|
||||
MODE_2_NO_MOSAIC(); \
|
||||
} \
|
||||
MODE_2_LOOP(MODE_2_MOSAIC, MODE_2_COORD_NO_OVERFLOW, BLEND, OBJWIN); \
|
||||
} else { \
|
||||
MODE_2_LOOP(MODE_2_NO_MOSAIC, MODE_2_COORD_NO_OVERFLOW, BLEND, OBJWIN); \
|
||||
|
@ -106,10 +112,14 @@ void GBAVideoSoftwareRendererDrawBackgroundMode3(struct GBAVideoSoftwareRenderer
|
|||
BACKGROUND_BITMAP_INIT;
|
||||
|
||||
uint32_t color = renderer->normalPalette[0];
|
||||
if (mosaicWait && localX >= 0 && localY >= 0) {
|
||||
LOAD_16(color, ((localX >> 8) + (localY >> 8) * renderer->masterEnd) << 1, renderer->d.vramBG[0]);
|
||||
color = mColorFrom555(color);
|
||||
}
|
||||
|
||||
int outX;
|
||||
for (outX = renderer->start; outX < renderer->end; ++outX) {
|
||||
BACKGROUND_BITMAP_ITERATE(renderer->masterEnd, GBA_VIDEO_VERTICAL_PIXELS);
|
||||
BACKGROUND_BITMAP_ITERATE(renderer->masterEnd, renderer->masterHeight);
|
||||
|
||||
if (!mosaicWait) {
|
||||
LOAD_16(color, ((localX >> 8) + (localY >> 8) * renderer->masterEnd) << 1, renderer->d.vramBG[0]);
|
||||
|
@ -144,6 +154,9 @@ void GBAVideoSoftwareRendererDrawBackgroundMode4(struct GBAVideoSoftwareRenderer
|
|||
if (GBARegisterDISPCNTIsFrameSelect(renderer->dispcnt)) {
|
||||
offset = 0xA000;
|
||||
}
|
||||
if (mosaicWait && localX >= 0 && localY >= 0) {
|
||||
color = ((uint8_t*)renderer->d.vramBG[0])[offset + (localX >> 8) + (localY >> 8) * renderer->masterEnd];
|
||||
}
|
||||
|
||||
int outX;
|
||||
for (outX = renderer->start; outX < renderer->end; ++outX) {
|
||||
|
@ -181,6 +194,10 @@ void GBAVideoSoftwareRendererDrawBackgroundMode5(struct GBAVideoSoftwareRenderer
|
|||
if (GBARegisterDISPCNTIsFrameSelect(renderer->dispcnt)) {
|
||||
offset = 0xA000;
|
||||
}
|
||||
if (mosaicWait && localX >= 0 && localY >= 0) {
|
||||
LOAD_16(color, offset + (localX >> 8) * 2 + (localY >> 8) * 320, renderer->d.vramBG[0]);
|
||||
color = mColorFrom555(color);
|
||||
}
|
||||
|
||||
int outX;
|
||||
for (outX = renderer->start; outX < renderer->end; ++outX) {
|
||||
|
|
|
@ -175,15 +175,21 @@ static inline void _compositeNoBlendNoObjwin(struct GBAVideoSoftwareRenderer* re
|
|||
int32_t y = background->sy + (renderer->start - 1) * background->dy; \
|
||||
int mosaicH = 0; \
|
||||
int mosaicWait = 0; \
|
||||
if (background->mosaic) { \
|
||||
int mosaicV = GBAMosaicControlGetBgV(renderer->mosaic) + 1; \
|
||||
y -= (inY % mosaicV) * background->dmy; \
|
||||
x -= (inY % mosaicV) * background->dmx; \
|
||||
mosaicH = GBAMosaicControlGetBgH(renderer->mosaic); \
|
||||
mosaicWait = renderer->start % (mosaicH + 1); \
|
||||
} \
|
||||
int32_t localX; \
|
||||
int32_t localY; \
|
||||
if (background->mosaic) { \
|
||||
int mosaicV = GBAMosaicControlGetBgV(renderer->mosaic) + 1; \
|
||||
mosaicH = GBAMosaicControlGetBgH(renderer->mosaic) + 1; \
|
||||
mosaicWait = (mosaicH - renderer->start + GBA_VIDEO_HORIZONTAL_PIXELS * mosaicH) % mosaicH; \
|
||||
int32_t startX = renderer->start - (renderer->start % mosaicH); \
|
||||
--mosaicH; \
|
||||
localX = -(inY % mosaicV) * background->dmx; \
|
||||
localY = -(inY % mosaicV) * background->dmy; \
|
||||
x += localX; \
|
||||
y += localY; \
|
||||
localX += background->sx + startX * background->dx; \
|
||||
localY += background->sy + startX * background->dy; \
|
||||
} \
|
||||
\
|
||||
uint32_t flags = (background->priority << OFFSET_PRIORITY) | (background->index << OFFSET_INDEX) | FLAG_IS_BACKGROUND; \
|
||||
flags |= FLAG_TARGET_2 * background->target2; \
|
||||
|
|
|
@ -60,7 +60,9 @@ static void _switchMode(struct GBASIO* sio) {
|
|||
if (sio->activeDriver && sio->activeDriver->unload) {
|
||||
sio->activeDriver->unload(sio->activeDriver);
|
||||
}
|
||||
mLOG(GBA_SIO, DEBUG, "Switching mode from %s to %s", _modeName(sio->mode), _modeName(newMode));
|
||||
if (sio->mode != (enum GBASIOMode) -1) {
|
||||
mLOG(GBA_SIO, DEBUG, "Switching mode from %s to %s", _modeName(sio->mode), _modeName(newMode));
|
||||
}
|
||||
sio->mode = newMode;
|
||||
sio->activeDriver = _lookupDriver(sio, sio->mode);
|
||||
if (sio->activeDriver && sio->activeDriver->load) {
|
||||
|
|
|
@ -153,7 +153,10 @@ void GBAVideoAssociateRenderer(struct GBAVideo* video, struct GBAVideoRenderer*
|
|||
renderer->writeVideoRegister(renderer, REG_DISPCNT, video->p->memory.io[REG_DISPCNT >> 1]);
|
||||
renderer->writeVideoRegister(renderer, REG_GREENSWP, video->p->memory.io[REG_GREENSWP >> 1]);
|
||||
int address;
|
||||
for (address = REG_BG0CNT; address < REG_SOUND1CNT_LO; address += 2) {
|
||||
for (address = REG_BG0CNT; address < 0x56; address += 2) {
|
||||
if (address == 0x4E) {
|
||||
continue;
|
||||
}
|
||||
renderer->writeVideoRegister(renderer, address, video->p->memory.io[address >> 1]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -787,7 +787,7 @@ void CoreController::scanCard(const QString& path) {
|
|||
QFile file(path);
|
||||
file.open(QIODevice::ReadOnly);
|
||||
m_eReaderData = file.read(2912);
|
||||
} else if (image.size() == QSize(989, 44)) {
|
||||
} else if (image.size() == QSize(989, 44) || image.size() == QSize(639, 44)) {
|
||||
const uchar* bits = image.constBits();
|
||||
size_t size;
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(5, 10, 0))
|
||||
|
|
|
@ -34,8 +34,6 @@ for i in range(16):
|
|||
for i in range(16):
|
||||
gg[i] = rev[gg[i]]
|
||||
|
||||
print(*[hex(x) for x in gg])
|
||||
|
||||
def interleave(data, header):
|
||||
data = data.reshape([-1, 48]).T
|
||||
new_data = np.zeros((64, data.shape[1]), dtype=data.dtype)
|
||||
|
@ -88,67 +86,69 @@ def bin2raw(data):
|
|||
new_data = interleave(np.frombuffer(data, np.uint8), header)
|
||||
return new_data.tobytes()
|
||||
|
||||
with open(sys.argv[1], 'rb') as f:
|
||||
data = f.read()
|
||||
size = len(data)
|
||||
|
||||
if size in (1344, 2112):
|
||||
data = bin2raw(data)
|
||||
def make_dotcode(data):
|
||||
size = len(data)
|
||||
with open('dotcode.raw', 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
blocks = size // blocksize
|
||||
height = 36
|
||||
width = 35
|
||||
margin = 2
|
||||
if size in (1344, 2112):
|
||||
data = bin2raw(data)
|
||||
size = len(data)
|
||||
|
||||
dots = np.zeros((width * blocks + margin * 2 + 1, height + margin * 2), dtype=np.bool)
|
||||
anchor = np.array([[0, 1, 1, 1, 0],
|
||||
[1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1],
|
||||
[0, 1, 1, 1, 0]], dtype=np.bool)
|
||||
alignment = np.array([1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0], dtype=np.bool)
|
||||
nybbles = [[0, 0, 0, 0, 0], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [1, 0, 0, 1, 0],
|
||||
[0, 0, 1, 0, 0], [0, 0, 1, 0, 1], [0, 0, 1, 1, 0], [1, 0, 1, 1, 0],
|
||||
[0, 1, 0, 0, 0], [0, 1, 0, 0, 1], [0, 1, 0, 1, 0], [1, 0, 1, 0, 0],
|
||||
[0, 1, 1, 0, 0], [0, 1, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 0]]
|
||||
addr = [0x03FF]
|
||||
for i in range(1, 54):
|
||||
addr.append(addr[i - 1] ^ ((i & -i) * 0x769))
|
||||
if (i & 0x07) == 0:
|
||||
addr[i] ^= 0x769
|
||||
if (i & 0x0F) == 0:
|
||||
addr[i] ^= 0x769 << 1
|
||||
if (i & 0x1F) == 0:
|
||||
addr[i] ^= (0x769 << 2) ^ 0x769
|
||||
blocks = size // blocksize
|
||||
height = 36
|
||||
width = 35
|
||||
margin = 2
|
||||
|
||||
base = 1 if blocks == 18 else 25
|
||||
for i in range(blocks + 1):
|
||||
dots[i * width:i * width + 5, 0:5] = anchor
|
||||
dots[i * width:i * width + 5, height + margin * 2 - 5:height + margin * 2] = anchor
|
||||
dots[i * width + margin, margin + 5] = 1
|
||||
a = addr[base + i]
|
||||
for j in range(16):
|
||||
dots[i * width + margin, margin + 14 + j] = a & (1 << (15 - j))
|
||||
for i in range(blocks):
|
||||
dots[i * width:(i + 1) * width, margin] = alignment
|
||||
dots[i * width:(i + 1) * width, height + margin - 1] = alignment
|
||||
block = []
|
||||
for byte in data[i * blocksize:(i + 1) * blocksize]:
|
||||
block.extend(nybbles[byte >> 4])
|
||||
block.extend(nybbles[byte & 0xF])
|
||||
j = 0
|
||||
for y in range(3):
|
||||
dots[i * width + margin + 5:i * width + margin + 31, margin + 2 + y] = block[j:j + 26]
|
||||
j += 26
|
||||
for y in range(26):
|
||||
dots[i * width + margin + 1:i * width + margin + 35, margin + 5 + y] = block[j:j + 34]
|
||||
j += 34
|
||||
for y in range(3):
|
||||
dots[i * width + margin + 5:i * width + margin + 31, margin + 31 + y] = block[j:j + 26]
|
||||
j += 26
|
||||
im = PIL.Image.fromarray(dots.T)
|
||||
dots = np.zeros((width * blocks + margin * 2 + 1, height + margin * 2), dtype=bool)
|
||||
anchor = np.array([[0, 1, 1, 1, 0],
|
||||
[1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1],
|
||||
[0, 1, 1, 1, 0]], dtype=bool)
|
||||
alignment = np.array([1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0], dtype=bool)
|
||||
nybbles = [[0, 0, 0, 0, 0], [0, 0, 0, 0, 1], [0, 0, 0, 1, 0], [1, 0, 0, 1, 0],
|
||||
[0, 0, 1, 0, 0], [0, 0, 1, 0, 1], [0, 0, 1, 1, 0], [1, 0, 1, 1, 0],
|
||||
[0, 1, 0, 0, 0], [0, 1, 0, 0, 1], [0, 1, 0, 1, 0], [1, 0, 1, 0, 0],
|
||||
[0, 1, 1, 0, 0], [0, 1, 1, 0, 1], [1, 0, 0, 0, 1], [1, 0, 0, 0, 0]]
|
||||
addr = [0x03FF]
|
||||
for i in range(1, 54):
|
||||
addr.append(addr[i - 1] ^ ((i & -i) * 0x769))
|
||||
if (i & 0x07) == 0:
|
||||
addr[i] ^= 0x769
|
||||
if (i & 0x0F) == 0:
|
||||
addr[i] ^= 0x769 << 1
|
||||
if (i & 0x1F) == 0:
|
||||
addr[i] ^= (0x769 << 2) ^ 0x769
|
||||
|
||||
base = 1 if blocks == 18 else 25
|
||||
for i in range(blocks + 1):
|
||||
dots[i * width:i * width + 5, 0:5] = anchor
|
||||
dots[i * width:i * width + 5, height + margin * 2 - 5:height + margin * 2] = anchor
|
||||
dots[i * width + margin, margin + 5] = 1
|
||||
a = addr[base + i]
|
||||
for j in range(16):
|
||||
dots[i * width + margin, margin + 14 + j] = a & (1 << (15 - j))
|
||||
for i in range(blocks):
|
||||
dots[i * width:(i + 1) * width, margin] = alignment
|
||||
dots[i * width:(i + 1) * width, height + margin - 1] = alignment
|
||||
block = []
|
||||
for byte in data[i * blocksize:(i + 1) * blocksize]:
|
||||
block.extend(nybbles[byte >> 4])
|
||||
block.extend(nybbles[byte & 0xF])
|
||||
j = 0
|
||||
for y in range(3):
|
||||
dots[i * width + margin + 5:i * width + margin + 31, margin + 2 + y] = block[j:j + 26]
|
||||
j += 26
|
||||
for y in range(26):
|
||||
dots[i * width + margin + 1:i * width + margin + 35, margin + 5 + y] = block[j:j + 34]
|
||||
j += 34
|
||||
for y in range(3):
|
||||
dots[i * width + margin + 5:i * width + margin + 31, margin + 31 + y] = block[j:j + 26]
|
||||
j += 26
|
||||
return np.pad(dots.T, 2)
|
||||
|
||||
with open(sys.argv[1], 'rb') as f:
|
||||
dots = make_dotcode(f.read())
|
||||
|
||||
im = PIL.Image.fromarray(dots)
|
||||
im = PIL.ImageChops.invert(im)
|
||||
im.save('dotcode.png')
|
||||
im.save('dotcode.bmp')
|
||||
|
|
Loading…
Reference in New Issue