├── .gitignore ├── Flipnote Player.py ├── Hatenatools ├── License.txt ├── NTFT.py ├── PPM.py ├── ReadMe.txt ├── UGO.py ├── __init__.py └── hatenatools project page.url ├── License.txt ├── ReadMe.txt ├── compile.bat ├── config.ini ├── graphics ├── bg.png ├── browserbg.png ├── button.png ├── drive icon.png ├── folder icon.png ├── icon base.png ├── icon.png ├── locked.png ├── profontwindows.ttf ├── scrollbarbg.png ├── scroller.png ├── servers │ ├── pbhatena.png │ └── sudomemo.png └── spinoff.png └── icon.ico /.gitignore: -------------------------------------------------------------------------------- 1 | #ignore patterns 2 | *_bak 3 | *.pyc 4 | *.pyd 5 | *.pyo 6 | *.lnk 7 | *.ppm 8 | #*.bat 9 | *.pdn 10 | 11 | #specific files/folders: 12 | get fps for flipnotes 13 | hatenafetch 14 | tile.png 15 | Untitled.png 16 | error.txt 17 | 18 | # OS generated files # 19 | .DS_Store 20 | .DS_Store? 21 | ._* 22 | .Spotlight-V100 23 | .Trashes 24 | ehthumbs.db 25 | Thumbs.db -------------------------------------------------------------------------------- /Flipnote Player.py: -------------------------------------------------------------------------------- 1 | # Flipnote Player by pbsds 2 | 3 | DEBUG = False 4 | 5 | import pygame, sys, numpy as np, ConfigParser, time, os, glob, urllib2, random 6 | from scikits.samplerate import resample as SciResample 7 | from Hatenatools import PPM, UGO, NTFT 8 | 9 | if "win" in sys.platform: 10 | from ctypes import windll 11 | import win32api 12 | def GetDriveNames(): 13 | bitmask = windll.kernel32.GetLogicalDrives() 14 | for i, letter in enumerate(map(chr, xrange(ord("A"), ord("Z")+1))): 15 | if bitmask & (1< 0: 221 | if DEBUG: print "up" 222 | self.scroll -= 1 223 | 224 | self.step() 225 | 226 | #push to screen: 227 | pygame.display.flip() 228 | def step(self): 229 | mx, my = pygame.mouse.get_pos() 230 | mclick = pygame.mouse.get_pressed()[0] 231 | if mclick and self.prevmclick: 232 | self.prevmclick = mclick 233 | mclick = False 234 | else: 235 | self.prevmclick = mclick 236 | 237 | #bottom buttons: 238 | for i in xrange(2): 239 | y = 50*i#pixel offset for buttons/labels 240 | b = 4*i#self.buttons and self.labels offset 241 | 242 | if 445+y<=my<495+y: 243 | for i in xrange(4): 244 | updatelabel = False 245 | 246 | if 16+i*128<=mx<144+i*128: 247 | if mclick: 248 | if not y: 249 | if i == 0:#play/stop 250 | if self.playing: 251 | self.StopPPM() 252 | else: 253 | self.PlayPPM() 254 | updatelabel = True 255 | if i == 1:#loop toggle: 256 | self.loop = not self.loop 257 | self.labels[1] = self.lLoop[1*self.loop] 258 | updatelabel = True 259 | if i == 2:#previous frame 260 | if self.ppm: 261 | if self.playing: 262 | self.StopPPM() 263 | updatelabel = True 264 | self.frame -= 1 265 | if self.frame < 0: 266 | if self.loop: 267 | self.frame = self.ppm.FrameCount-1 268 | else: 269 | self.frame = 0 270 | self.UpdateFrame() 271 | if i == 3:#next frame 272 | if self.ppm: 273 | if self.playing: 274 | self.StopPPM() 275 | updatelabel = True 276 | self.frame += 1 277 | if self.frame >= self.ppm.FrameCount: 278 | if self.loop: 279 | self.frame = 0 280 | else: 281 | self.frame = self.ppm.FrameCount-1 282 | self.UpdateFrame() 283 | else: 284 | if i == 0:#save flipnote 285 | if self.ppm and self.dir[:7] == "http://":#todo, make a message on screen 286 | print "saved to", "saved/%i - %s" % (int(time.time()), self.ppm.CurrentFilename) 287 | f = open("saved/%i - %s" % (int(time.time()), self.ppm.CurrentFilename), "wb") 288 | f.write(self.ppmRAW) 289 | f.close() 290 | if i == 1:#toogle scale 291 | self.DoScale = not self.DoScale 292 | if not self.browsing: 293 | self.UpdateFrame(bg=True) 294 | if i == 2:#toggle scaler 295 | self.MAME = not self.MAME 296 | if not self.browsing: 297 | self.UpdateFrame() 298 | if i == 3:#switch view 299 | if self.browsing and self.ppm: 300 | self.labels[7] = self.lRest[5] 301 | pygame.draw.rect(self.window, (128, 192, 6), (528, 72, 16, 328)) 302 | self.browsing = False 303 | self.UpdateFrame() 304 | else: 305 | self.GotoBrowser() 306 | updatelabel = True 307 | 308 | pass#do something 309 | 310 | if not self.buttons[i+b]: 311 | self.buttons[i+b] = True 312 | updatelabel = True 313 | else: 314 | if self.buttons[i+b] == True: 315 | self.buttons[i+b] = False 316 | updatelabel = True 317 | 318 | if updatelabel or self.redrawBottomButtons: 319 | self.window.blit(self.sButton[1*self.buttons[i+b]], (16+i*128, 445+y)) 320 | if self.labels[i+b]: 321 | self.window.blit(self.labels[i+b], (80+i*128-self.labels[i+b].get_width()/2, 445+18+y)) 322 | else: 323 | if True in self.buttons or self.redrawBottomButtons: 324 | for i in xrange(4): 325 | self.window.blit(self.sButton[0], (16+i*128, 445+y)) 326 | if self.labels[i+b]: 327 | self.window.blit(self.labels[i+b], (80+i*128-self.labels[i+b].get_width()/2, 445+18+y)) 328 | self.buttons[i+b] = False 329 | self.redrawBottomButtons = False 330 | 331 | #playback: 332 | if self.playing: 333 | if not self.epoch: self.epoch = time.time() 334 | 335 | frame = (time.time() - self.epoch) / self.speed2period[self.ppm.Framespeed] 336 | if frame >= self.tframe + 1: 337 | self.tframe = int(frame)#rounding down 338 | self.frame = self.tframe % self.ppm.FrameCount 339 | if DEBUG: print self.tframe, self.frame 340 | 341 | if self.frame == 0 and self.tframe <> 0 and not self.loop: 342 | self.StopPPM() 343 | self.frame = self.ppm.FrameCount-1 344 | return 345 | 346 | #play sounds: 347 | if self.frame == 0 and self.sounds[0]: 348 | self.sounds[0].stop() 349 | 350 | self.sounds[0].play() 351 | if DEBUG: print "play bgm" 352 | for i, play in enumerate(self.ppm.SFXUsage[self.frame]): 353 | if play and self.sounds[i+1]: 354 | self.sounds[i+1].play() 355 | if DEBUG: print "play sfx", i+1 356 | 357 | #draw frame: 358 | self.UpdateFrame() 359 | 360 | #browser: 361 | elif self.browsing: 362 | #self.browserS.fill((255, 255, 255)) 363 | self.browserS.blit(self.sBrowserBG, (0, 0)) 364 | 365 | #top bar: 366 | for i, label in enumerate(self.lBrowser): 367 | within = 16<=my<64 and 16+128*i<=mx<144+128*i 368 | 369 | self.browserS.blit(self.sButton[1*within], (i*128, 0)) 370 | if not (i==1 and not self.history): 371 | self.browserS.blit(label, (64+i*128-label.get_width()/2, 18)) 372 | 373 | if within and mclick: 374 | if i == 0: 375 | self.GotoDir(None) 376 | if i == 1: 377 | if self.history: 378 | dir, label = self.history.pop(-1) 379 | self.GotoDir(dir, label, True) 380 | if self.history: 381 | self.history.pop(-1) 382 | self.browserS.blit(self.labeldir[1], (256+10, 18)) 383 | 384 | #scrollbar: 385 | if self.scrollheld or (528<=mx<544 and 72+self.scroll*264/max(((len(self.files)-1)/2-4), 1)<=my<136+self.scroll*264/max(((len(self.files)-1)/2-4), 1)): 386 | scrollupdate = self.scroll <> self.prevscroll 387 | if not self.scrollheld: 388 | self.scrollerstate = 1 389 | scrollupdate = True 390 | if mclick: 391 | self.scrollheld = True 392 | self.scrollerstate = 2 393 | 394 | if self.scrollheld: 395 | newscroll = int(float(my-104) / (252. / float((len(self.files)-1)/2-4)) + 0.5) 396 | if newscroll <> self.scroll and 0<=newscroll<=(len(self.files)-1)/2-4: 397 | self.scroll = newscroll 398 | 399 | if not pygame.mouse.get_pressed()[0]: 400 | self.scrollheld = False 401 | scrollupdate = True 402 | self.scrollerstate = 1*(528<=mx<544 and 72+self.scroll*264/max(((len(self.files)-1)/2-4), 1)<=my<136+self.scroll*264/max(((len(self.files)-1)/2-4), 1)) 403 | 404 | if scrollupdate: 405 | self.window.blit(self.sScrollbarBG, (528, 72)) 406 | self.window.blit(self.sScroller[self.scrollerstate], (528, 72+self.scroll*264/max(((len(self.files)-1)/2-4), 1))) 407 | self.prevscroll = self.scroll 408 | elif self.scrollerstate or self.scroll <> self.prevscroll: 409 | self.scrollerstate = 0 410 | 411 | self.window.blit(self.sScrollbarBG, (528, 72)) 412 | self.window.blit(self.sScroller[self.scrollerstate], (528, 72+self.scroll*264/max(((len(self.files)-1)/2-4), 1))) 413 | self.prevscroll = self.scroll 414 | 415 | #content: 416 | #for i, (folder, name, label, icon, path) in enumerate(self.files): 417 | for i in xrange(10): 418 | if len(self.files) <= i+self.scroll*2: break 419 | folder, name, label, icon, path = self.files[i+self.scroll*2] 420 | y = i // 2 421 | x = i % 2 422 | 423 | #todo: scroll 424 | 425 | within = 20+256*x<=mx<268+256*x and 76+64*y<=my<140+64*y 426 | 427 | if within: 428 | pygame.draw.rect(self.browserS, (255, 255, 255), (4+256*x, 60+64*y, 248, 64)) 429 | 430 | self.browserS.blit(icon, (8+256*x, 64+64*y)) 431 | if label: 432 | if isinstance(label, tuple): 433 | for i, l in enumerate(label): 434 | if l: 435 | self.browserS.blit(l, (88+256*x, 68+16*i+64*y)) 436 | else: 437 | self.browserS.blit(label, (88+256*x, 84+64*y)) 438 | 439 | if within and mclick: 440 | if folder: 441 | self.GotoDir(path, name) 442 | else: 443 | self.PlayPPM(path) 444 | 445 | #push to screen: 446 | if self.browsing:#may have changed 447 | self.window.blit(self.browserS, (16, 16)) 448 | def UpdateVolume(self): 449 | for i in self.sounds: 450 | if i: i.set_volume(self.volume) 451 | #sub 452 | def UpdateFrame(self, bg=False): 453 | #trackbar: 454 | if self.ppm.FrameCount > 1: 455 | pygame.draw.rect(self.window, (72, 178, 245), (20, 421, 504*self.frame/(self.ppm.FrameCount-1), 16)) 456 | if not self.playing or self.frame==0: 457 | pygame.draw.rect(self.window, (245, 130, 6), (20+504*self.frame/(self.ppm.FrameCount-1), 421, 504-504*self.frame/(self.ppm.FrameCount-1), 16)) 458 | else: 459 | pygame.draw.rect(self.window, (245, 130, 6), (20, 421, 504, 16)) 460 | 461 | if bg: 462 | self.window.blit(self.sBG, (16, 16), (16, 16, 16+512, 16+384)) 463 | 464 | #frame itself 465 | if self.DoScale: 466 | if self.MAME: 467 | self.window.blit(pygame.transform.scale2x(self.frames[self.frame]), (16, 16)) 468 | else: 469 | self.window.blit(pygame.transform.scale(self.frames[self.frame], (512, 384)), (16, 16)) 470 | else: 471 | self.window.blit(self.frames[self.frame], (144, 112)) 472 | def GotoDir(self, dir, lappend=None, lforce=False): 473 | self.history.append((self.dir, self.labeldir[0])) 474 | self.dir = dir 475 | self.scroll = 0 476 | 477 | if dir==None: 478 | self.labeldir[0] = "root" 479 | self.history = [] 480 | elif self.labeldir[0] == "root" or lforce: 481 | self.labeldir[0] = lappend 482 | elif lappend: 483 | self.labeldir[0] = os.path.join(self.labeldir[0], lappend) 484 | self.labeldir[1] = Text.CreateShadowed(self.labeldir[0], 9, self.TextColor[0], self.TextColor[1]) 485 | 486 | self.files = []#[i] = (folder(bool), name(spr), label(surface), icon, full path) 487 | if dir == None: 488 | if len(glob.glob("saved/*.ppm")): 489 | self.files.append((True, "saved", Text.CreateShadowed("saved flipnotes", 9, self.TextColor[0], self.TextColor[1]), self.sIconFolder, "saved")) 490 | 491 | #self.files.append((True, "local", Text.CreateShadowed("local files", 9, self.TextColor[0], self.TextColor[1]), self.sIconFolder, os.getcwd()))#temp path 492 | 493 | 494 | for i, (letter, label) in enumerate(GetDriveNames()): 495 | self.files.append((True, "%s:"%letter, Text.CreateShadowed("%s (%s:/)" % (label, letter), 9, self.TextColor[0], self.TextColor[1]), self.sIconDrive, "%s:/"%letter))#temp path 496 | 497 | 498 | for name, address, port, icon in self.servers: 499 | self.files.append((True, "hatena", Text.CreateShadowed(name, 9, self.TextColor[0], self.TextColor[1]), icon, "prox://%s:%s" % (address, port))) 500 | 501 | #self.files.append((True, "local files", self.LoadIcon(), os.getcwd()))#temp path 502 | pass 503 | elif dir[4:7] == "://": 504 | if dir[:4] == "prox": 505 | urllib2.install_opener(urllib2.build_opener(urllib2.ProxyHandler({'http': dir[7:]}))) 506 | dir = "http://flipnote.hatena.com/ds/v2-eu/index.ugo" 507 | 508 | if dir[:4] == "http": 509 | ugo = UGO.UGO().Read(self.FetchHatena(dir)) 510 | 511 | for i in ugo.Items: 512 | if i[0] == "category": 513 | link, label, selected = i[1:] 514 | if not selected and link.split("?")[0][-4:] == ".uls": 515 | split = link.split("?") 516 | addr = split[0][:-4]+".ugo" + "?"*(len(split)>1) + "?".join(split[1:]) 517 | 518 | name = split[0][:-4].split("/")[-1] 519 | 520 | self.files.append((True, name, Text.CreateShadowed(label, 9, self.TextColor[0], self.TextColor[1]), self.sIconFolder, addr))#temp path 521 | 522 | 523 | elif i[0] == "button": 524 | trait, label, link, other, file = i[1:] 525 | 526 | if link.split("?")[0][-4:] == ".uls": 527 | split = link.split("?") 528 | addr = split[0][:-4]+".ugo" + "?"*(len(split)>1) + "?".join(split[1:]) 529 | 530 | name = split[0][:-4].split("/")[-1] 531 | 532 | if file: 533 | icon = self.LoadIcon(file[1])#ntft 534 | else: 535 | icon = self.sIconFolder 536 | 537 | self.files.append((True, name, Text.CreateShadowed(label, 9, self.TextColor[0], self.TextColor[1]), icon, addr))#temp path 538 | elif link[-4:] == ".ppm": 539 | icon = self.LoadIcon(file[1])#tmb 540 | 541 | username = Text.CreateShadowed(u"submitted by "+self.tmb.Username, 9, self.TextColor[0], self.TextColor[1]) 542 | filename = Text.CreateShadowed(file[0][:-4], 9, self.TextColor[0], self.TextColor[1]) 543 | stars = Text.CreateShadowed(other[0] + " stars", 9, self.StarColor[0], self.StarColor[1]) 544 | 545 | self.files.append((False, "flipnote", (filename, username, stars), icon, link)) 546 | else: 547 | folders = [] 548 | files = [] 549 | for i in glob.glob(os.path.join(dir, "*")): 550 | if os.path.isdir(i): 551 | folders.append(i) 552 | elif i[-4:] == ".ppm": 553 | files.append(i) 554 | 555 | folders.sort(key=lambda x: x.lower()) 556 | for i in folders: 557 | name = os.path.basename(i) 558 | self.files.append((True, name, Text.CreateShadowed(name, 9, self.TextColor[0], self.TextColor[1]), self.sIconFolder, i)) 559 | 560 | files.sort() 561 | for i in files: 562 | name = os.path.basename(i) 563 | 564 | f = open(i, "rb") 565 | icon = self.LoadIcon(f.read()) 566 | f.close() 567 | 568 | username = Text.CreateShadowed(u"submitted by "+self.tmb.Username, 9, self.TextColor[0], self.TextColor[1]) 569 | filename = Text.CreateShadowed(self.tmb.CurrentFilename[:-4], 9, self.TextColor[0], self.TextColor[1]) 570 | 571 | self.files.append((False, name, (username, filename), icon, i)) 572 | #public 573 | def PlayPPM(self, file=None, path=True): 574 | if file: 575 | if path: 576 | if file[:7] == "http://": 577 | self.LoadPPM(self.FetchHatena(file)) 578 | self.labels[4] = self.lRest[2] 579 | else: 580 | f = open(file, "rb") 581 | self.LoadPPM(f.read()) 582 | f.close() 583 | self.labels[4] = None 584 | self.redrawBottomButtons = True 585 | else: 586 | self.LoadPPM(file) 587 | else: 588 | if self.browsing and not self.ppm: 589 | return 590 | 591 | if not self.DoScale: 592 | self.window.blit(self.sBG, (16, 16), (16, 16, 16+512, 16+384)) 593 | 594 | self.browsing = False 595 | pygame.draw.rect(self.window, (128, 192, 6), (528, 72, 16, 328)) 596 | self.labels[7] = self.lRest[5] 597 | 598 | 599 | 600 | self.frame = -1 601 | self.playing = True 602 | self.labels[0] = self.lPlay[0] 603 | self.redrawBottomButtons = True 604 | def StopPPM(self): 605 | self.playing = False 606 | self.epoch = None 607 | #self.frame = -1 608 | self.tframe = -1 609 | for i in self.sounds: 610 | if i: i.stop() 611 | self.labels[0] = self.lPlay[1] 612 | self.redrawBottomButtons = True 613 | def GotoBrowser(self, root = False): 614 | self.StopPPM() 615 | self.browsing = True 616 | if self.ppm: self.labels[7] = self.lRest[6] 617 | 618 | self.window.blit(self.sScrollbarBG, (528, 72)) 619 | self.window.blit(self.sScroller[0], (528, 72+self.scroll*264/max(((len(self.files)-1)/2-4), 1))) 620 | 621 | if root: self.GotoDir(None) 622 | #internal 623 | def LoadPPM(self, data): 624 | self.clean() 625 | 626 | self.ppm = PPM.PPM().Read(data, DecodeThumbnail=True, ReadFrames=True, ReadSound=True) 627 | if not self.ppm: 628 | return False 629 | self.ppmRAW = data 630 | 631 | if DEBUG: print "speed",self.ppm.Framespeed, "with bgm recorded at",self.ppm.BGMFramespeed 632 | if DEBUG: print self.ppm.FrameCount, "frames" 633 | 634 | self.frames = [] 635 | for i in xrange(self.ppm.FrameCount): 636 | frame = self.ppm.GetFrame(i) 637 | 638 | #convert from 2d uint32 RGBA to 3d uint8 RGB: 639 | frame3d = np.zeros((256, 192, 3), dtype=">u1") 640 | frame3d[:,:,0] = frame >> 24 #red 641 | frame3d[:,:,1] = frame >> 16 & 0xFF#green 642 | frame3d[:,:,2] = frame >> 8 & 0xFF#blue 643 | #frame3d[:,:,3] = frame &0xFF#alpha 644 | 645 | self.frames.append(pygame.surfarray.make_surface(frame3d).convert()) 646 | 647 | 648 | for i in xrange(4): 649 | data = self.ppm.GetSound(i) 650 | if data: 651 | if DEBUG: print "sound index",i,len(data),"frames" 652 | 653 | dataNP = np.fromstring(data, dtype=np.int16) 654 | ratio = 1. / 8192.*44100. / self.speed2period[self.ppm.BGMFramespeed] * self.speed2period[self.ppm.Framespeed] 655 | #newsize = float(dataNP.shape[0]) * ratio 656 | 657 | if DEBUG: print dataNP.shape[0], "to", float(dataNP.shape[0]) * ratio, "@", ratio 658 | 659 | resampled = SciResample(dataNP, ratio, "sinc_fastest")#dtype here is now float32 660 | 661 | #scale down again: 662 | resampled = resampled * (float(dataNP.max())/float(resampled.max())) 663 | 664 | #convert to sterio and pass to pygame: 665 | stereo = np.zeros((resampled.shape[0],2), dtype=np.int16) 666 | stereo[:,0] = resampled 667 | stereo[:,1] = resampled 668 | self.sounds[i] = pygame.mixer.Sound(buffer(stereo.tostring())) 669 | self.UpdateVolume() 670 | 671 | self.loop = self.ppm.Looped 672 | self.labels[1] = self.lLoop[1*self.ppm.Looped] 673 | def clean(self):#remove current loaded ppm 674 | self.StopPPM() 675 | 676 | #pygame: 677 | for i in xrange(len(self.frames)-1,-1,-1): 678 | del self.frames[i] 679 | for i in xrange(4): 680 | if self.sounds[i]: 681 | del self.sounds[i] 682 | self.sounds.insert(i, None) 683 | 684 | del self.ppm, self.ppmRAW 685 | self.ppm = None 686 | self.ppmRAW = None 687 | def LoadIcon(self, data, locked=False, spinoff=False, gettmb=True):#returns it 688 | if data[:4] == "PARA":#flipnote 689 | self.tmb.Read(data, True) 690 | img = self.tmb.GetThumbnail(force=True) 691 | 692 | if gettmb: 693 | locked = self.tmb.Locked 694 | if not (self.tmb.OriginalAuthorID == self.tmb.EditorAuthorID == self.tmb.PreviousEditAuthorID): 695 | spinoff = True 696 | else:#NTFT 697 | self.ntft.Read(data, (32, 32)) 698 | img = self.ntft.Image 699 | 700 | #convert to surface: 701 | img3d = np.zeros((img.shape[0], img.shape[1], 3), dtype=">u1") 702 | img3d[:,:,0] = img >> 24 #red 703 | img3d[:,:,1] = img >> 16 & 0xFF#green 704 | img3d[:,:,2] = img >> 8 & 0xFF#blue 705 | #img3d[:,:,3] = img & 0xFF#alpha 706 | 707 | surf = pygame.surfarray.make_surface(img3d[:,:,:3]) 708 | 709 | if data[:4] <> "PARA":#ntft alpha: 710 | surf = surf.convert_alpha() 711 | a = pygame.surfarray.pixels_alpha(surf) 712 | a[:,:] = img & 0xFF#alpha 713 | del a 714 | del img, img3d 715 | 716 | if locked: 717 | surf.blit(self.sLocked, (47, 30)) 718 | if spinoff: 719 | surf.blit(self.sSpinOff, (2, 34)) 720 | 721 | return self.CreateIcon(surf) 722 | def CreateIcon(self, surface): 723 | out = self.sIconBase.copy() 724 | out.blit(surface, (4+32-surface.get_width()/2, 4+24-surface.get_height()/2)) 725 | return out 726 | def FetchHatena(self, addr): 727 | if DEBUG: print "hatenafetch:", addr 728 | 729 | #sudomemo fix. Sudomemo blocks requests with user-agents and requires each session to have a unique SID 730 | if addr == "http://flipnote.hatena.com/ds/v2-eu/index.ugo": 731 | self.SID = 'pbsdsFlipnotePlayer%s' % "".join((chr(random.randrange(ord("A"), ord("Z")+1) + random.choice((0, ord("a")-ord("A")))) for _ in xrange(21))) 732 | if DEBUG: print "generated SID:", self.SID 733 | urllib2._opener.addheaders = [] 734 | 735 | request = urllib2.Request(addr, headers={'x-dsi-sid': self.SID}) 736 | 737 | resp = urllib2.urlopen(request) 738 | 739 | data = resp.read() 740 | 741 | if 0 and addr.split("/")[-1].split("?")[0][-4:] == ".ugo": 742 | 743 | print "writing ugo...", 744 | f = open("hatenafetch/"+addr.split("/")[-1].replace("?", "."), "wb") 745 | f.write(data) 746 | f.close() 747 | 748 | print "writing ugoxml..." 749 | name = addr.split("/")[-1].split("?")[0]+ "xml" + "."*(len(addr.split("/")[-1].split("?")) > 1) + ".".join(addr.split("/")[-1].split("?")[1:]) 750 | UGO.UGO().Read(data).WriteXML(xmlname="hatenafetch/"+name, folder=("%s embedded" % name)) 751 | 752 | print "done" 753 | 754 | return data 755 | #events: 756 | def save(self): 757 | self.ini.set("settings", "volume", str(int(self.volume*1000+0.5))) 758 | self.ini.set("settings", "scale", str(self.DoScale)) 759 | self.ini.set("settings", "AdvanceMameScaler", str(self.MAME)) 760 | 761 | f = open("config.ini", "w") 762 | self.ini.write(f) 763 | f.close() 764 | 765 | def main(): 766 | program = player() 767 | 768 | #if "opened with" a ppm file: 769 | if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]): 770 | program.GotoDir(os.path.dirname(sys.argv[1])) 771 | program.PlayPPM(sys.argv[1]) 772 | 773 | program.MainLoop()#blocking 774 | 775 | print "ech" 776 | 777 | program.save() 778 | 779 | def excepthook(type, value, traceb): 780 | import traceback 781 | class dummy: 782 | buff = "" 783 | def write(self, data): 784 | self.buff += data 785 | 786 | dummyf = dummy() 787 | traceback.print_exception(type, value, traceb, file=dummyf) 788 | f = open("error.txt", "w") 789 | f.write(dummyf.buff) 790 | f.close() 791 | print dummyf.buff 792 | print "Error message written to error.txt" 793 | sys.exit() 794 | if(__name__ == '__main__'): 795 | sys.excepthook = excepthook 796 | main() 797 | -------------------------------------------------------------------------------- /Hatenatools/License.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /Hatenatools/NTFT.py: -------------------------------------------------------------------------------- 1 | #NTFT.py by pbsds 2 | #AGPL3 licensed 3 | # 4 | #PIL is required to read and write images to disk 5 | # 6 | #Credits: 7 | # 8 | # -The guys behind TiledGGD. This sped up my work a lit. 9 | # -Jsafive for supplying .ugo files 10 | # 11 | import sys, os, numpy as np 12 | try: 13 | import Image 14 | hasPIL = True 15 | except ImportError: 16 | hasPIL = False 17 | 18 | 19 | #helpers 20 | def AscDec(ascii, LittleEndian=False):#Converts a ascii string into a decimal 21 | ret = 0 22 | l = map(ord, ascii) 23 | if LittleEndian: l.reverse() 24 | for i in l: 25 | ret = (ret<<8) | i 26 | return ret 27 | def DecAsc(dec, length=None, LittleEndian=False):#Converts a decimal into an ascii string of chosen length 28 | out = [] 29 | while dec <> 0: 30 | out.insert(0, dec&0xFF) 31 | dec >>= 8 32 | #"".join(map(chr, out)) 33 | 34 | if length: 35 | if len(out) > length: 36 | #return "".join(map(chr, out[-length:])) 37 | out = out[-length:] 38 | if len(out) < length: 39 | #return "".join(map(chr, [0]*(length-len(out)) + out)) 40 | out = [0]*(length-len(out)) + out 41 | 42 | if LittleEndian: out.reverse() 43 | return "".join(map(chr, out)) 44 | def clamp(value, min, max): 45 | if value > max: return max 46 | if value < min: return min 47 | return value 48 | 49 | #Class NTFT: 50 | # 51 | # The NTFT image format stores RGB values as 5 bits each: a value between 0 and 31. 52 | # It has 1 bit of transparancy, which means its either vidssible or invisible. No gradients. 53 | # 54 | # How to use: 55 | # 56 | # Converting to NTFT file: 57 | # 58 | # image = ReadImage(input_path) 59 | # NTFT().SetImage(image).WriteFile(output_path) 60 | # 61 | # Reading NTFT file: 62 | # 63 | # ntft = NTFT().ReadFile(input_path, (width, height)) 64 | # WriteImage(ntft.Image, output_path) 65 | # 66 | class NTFT: 67 | def __init__(self): 68 | self.Loaded = False 69 | def ReadFile(self, path, size): 70 | f = open(path, "rb") 71 | ret = self.Read(f.read(), size) 72 | f.close() 73 | return ret 74 | def Read(self, data, (w, h)): 75 | #the actual stored data is a image with the sizes padded to the nearest power of 2. The image is then clipped out from it. 76 | psize = [] 77 | for i in (w, h): 78 | p = 1 79 | while 1<

len(data): 86 | print "Invalid sizes" 87 | return False 88 | 89 | #JUST DO IT! 90 | #self.Image = [[None for _ in xrange(h)] for _ in xrange(w)] 91 | self.Image = np.zeros((w, h), dtype=">u4") 92 | for y in xrange(h): 93 | for x in xrange(w): 94 | pos = (x + y*pw)*2 95 | byte = AscDec(data[pos:pos+2], True) 96 | 97 | #ARGB1555 -> RGBA8: 98 | a = (byte >> 15 ) * 0xFF 99 | b = (byte >> 10 & 0x1F) * 0xFF / 0x1F 100 | g = (byte >> 5 & 0x1F) * 0xFF / 0x1F 101 | r = (byte & 0x1F) * 0xFF / 0x1F 102 | 103 | #self.Image[x][y] = (r<<24) | (g<<16) | (b<<8) | a#RGBA8 104 | self.Image[x, y] = (r<<24) | (g<<16) | (b<<8) | a#RGBA8 105 | 106 | self.Loaded = True 107 | return self 108 | def WriteFile(self, path): 109 | if self.Loaded: 110 | f = open(path, "wb") 111 | f.write(self.Pack()) 112 | f.close() 113 | return True 114 | else: 115 | return False 116 | def Pack(self): 117 | if not self.Loaded: 118 | return False 119 | 120 | #h = len(self.Image[0]) 121 | #w = len(self.Image) 122 | w, h = self.Image.shape 123 | 124 | #the actual stored data is a image with the sizes padded to the nearest power of 2 125 | psize = [] 126 | for i in (w, h): 127 | p = 1 128 | while 1<

> 24 139 | g = (c >> 16) & 0xFF 140 | b = (c >> 8 ) & 0xFF 141 | a = c & 0xFF 142 | 143 | #convert 144 | a = 1 if a >= 0x80 else 0 145 | r = r * 0x1F / 0xFF 146 | g = g * 0x1F / 0xFF 147 | b = b * 0x1F / 0xFF 148 | 149 | #store 150 | out.append(DecAsc((a<<15) | (b<<10) | (g<<5) | r, 2, True)) 151 | 152 | return "".join(out) 153 | def SetImage(self, Image): 154 | self.Image = Image 155 | self.Loaded = True 156 | return self 157 | 158 | #Function WriteImage: 159 | # 160 | # Writes a 2D array of uint32 RGBA values as a image file. 161 | # Designed to work with NTFT.Image 162 | # 163 | # This function requires the PIl imaging module 164 | def WriteImage(image, outputPath): 165 | if not hasPIL: 166 | print "Error: PIL not found!" 167 | return False 168 | #if not image: return False 169 | 170 | out = image.tostring("F") 171 | 172 | # out = [] 173 | # for y in xrange(len(image[0])): 174 | # for x in xrange(len(image)): 175 | # out.append(DecAsc(image[x][y], 4)) 176 | 177 | out = Image.fromstring("RGBA", (len(image), len(image[0])), out) 178 | 179 | filetype = outputPath[outputPath.rfind(".")+1:] 180 | out.save(outputPath, filetype) 181 | 182 | return True 183 | 184 | #Function ReadImage: 185 | # 186 | # Returns a 2D list of uint32 RGBA values of the image file. 187 | # This can be passed into NTFT().SetImage() 188 | # 189 | # This function requires the PIl imaging module 190 | def ReadImage(path):#TODO: make it support numpy 191 | if not hasPIL: return False 192 | 193 | image = Image.open(path) 194 | pixeldata = image.getdata() 195 | w, h = image.size 196 | 197 | if len(pixeldata[0]) < 4: 198 | def Combine((r, g, b)): 199 | return (r << 24) | (g << 16) | (b << 8) | 0xFF 200 | else: 201 | def Combine((r, g, b, a)): 202 | return (r << 24) | (g << 16) | (b << 8) | a 203 | 204 | #ret = [] 205 | ret = np.zeros((w, h), dtype=">u4") 206 | for x in xrange(w): 207 | #line = [] 208 | for y in xrange(h): 209 | ret[x, y] = Combine(pixeldata[y*w + x])#maybe make a more numpy efficient way? 210 | #line.append(Combine(pixeldata[y*w + x])) 211 | #ret.append(line) 212 | 213 | return ret 214 | 215 | 216 | 217 | #testing: 218 | # i = NTFT().ReadFile("NTFTtests/kaeru.ntft", (36, 30)) 219 | # WriteImage(i.Image, "NTFTtests/kaeru.png") 220 | 221 | # i = NTFT().ReadFile("NTFTtests/News.ntft", (32, 32)) 222 | # WriteImage(i.Image, "NTFTtests/News.png") 223 | 224 | # i = NTFT().ReadFile("NTFTtests/Special Room.ntft", (32, 32)) 225 | # WriteImage(i.Image, "NTFTtests/Special Room.png") 226 | 227 | #i = NTFT() 228 | #i.Loaded = True 229 | #i.Image = ReadImage("NTFTtests/geh.png") 230 | #i.WriteFile("NTFTtests/geh.ntft") 231 | 232 | if __name__ == "__main__": 233 | print " == NTFT.py ==" 234 | print " == by pbsds ==" 235 | print " == v0.95 ==" 236 | print 237 | 238 | if not hasPIL: 239 | print "PIL not found! Exiting..." 240 | sys.exit() 241 | 242 | if len(sys.argv) < 2: 243 | print "Usage:" 244 | print " NTFT.py [ [ ]]" 245 | print "" 246 | print "Can convert a NTFT to PNG or the other way around." 247 | print "if isn't specified it will be set to with an another extension" 248 | print "" 249 | print "The NTFT file contain only the colordata, so it's up to the user to find or" 250 | print "store the resolution of the image. and is required" 251 | print "to convert a NTFT file to a image." 252 | print "32x32 is the normal resolution for button icons in UGO files." 253 | sys.exit() 254 | 255 | input = sys.argv[1] 256 | 257 | if input[-4:].lower() == "ntft" or len(sys.argv) >= 5: 258 | print "Mode: NTFT -> image" 259 | Encode = False 260 | else: 261 | print "Mode: image -> NTFT" 262 | Encode = True#if false it'll decode 263 | 264 | if len(sys.argv) >= 3: 265 | output = sys.argv[2] 266 | 267 | width, height = None, None 268 | if len(sys.argv) >= 5: 269 | if (not sys.argv[3].isdigit()) or (not sys.argv[4].isdigit()): 270 | print "Invalid size input!" 271 | sys.exit() 272 | width = int(sys.argv[3]) 273 | height = int(sys.argv[4]) 274 | 275 | if not (width and height) and not Encode: 276 | print "Image size not provided!" 277 | sys.exit() 278 | 279 | else: 280 | output = ".".join(input.split(".")[:-1]) + (".ntft" if Encode else ".png") 281 | 282 | print "Converting..." 283 | if Encode: 284 | try: 285 | image = ReadImage(input) 286 | except IOError as err: 287 | print err 288 | sys.exit() 289 | 290 | i = NTFT() 291 | i.Loaded = True 292 | i.Image = image 293 | i.WriteFile(output) 294 | else: 295 | try: 296 | ntft = NTFT().ReadFile(input, (width, height)) 297 | except IOError as err: 298 | print err 299 | sys.exit() 300 | 301 | if not ntft:#eeror message already printed 302 | sys.exit() 303 | 304 | WriteImage(ntft.Image, output) 305 | print "Done!" -------------------------------------------------------------------------------- /Hatenatools/PPM.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #PPM.py by pbsds for python 2.7 3 | #AGPL3 licensed 4 | # 5 | #Numpy is required 6 | #PIL is needed to write images to disk 7 | # 8 | #Credits: 9 | # 10 | # - Steven for most of the documentation on DSiBrew and his frame decoding example on his talkpage 11 | # - Remark for help on the 8x8 tiling on the preview image. 12 | # - Jsafive for supplying .tmb files 13 | # - Austin Burk, Midmad on hatena haiku and WDLmaster on hcs64.com for determining the sound codec 14 | # 15 | import sys, wave, audioop, re, os #needs os and time aswell in CMD mode (UPDATE 2018/07/04: os used to get devnull for discarding subprocess output) 16 | import numpy as np 17 | import subprocess # used for ffmpeg 18 | import tempfile, shutil # temporary directory whilst exporting, shutil to clean up after 19 | import time 20 | try: 21 | from PIL import Image 22 | hasPIL = True 23 | except ImportError: 24 | print "Warning: PIL not found, image extraction won't work!" 25 | hasPIL = False 26 | # Test for ffmpeg by executing ffmpeg -h; an OSError indicates ffmpeg is not installed 27 | 28 | try: 29 | with open(os.devnull,"w") as null: 30 | subprocess.call(["ffmpeg","-h"], 31 | stdout=null, 32 | stderr=null) 33 | hasffmpeg = True 34 | except OSError: 35 | print "Warning: ffmpeg not found, video export unavailable. Please make sure ffmepg is installed and can be accessed on your path." 36 | hasffmpeg = False 37 | 38 | #helpers: 39 | def AscDec(ascii, LittleEndian=False):#Converts a ascii string into a decimal 40 | ret = 0 41 | l = map(ord, ascii) 42 | if LittleEndian: l.reverse() 43 | for i in l: 44 | ret = (ret<<8) | i 45 | return ret 46 | def DecAsc(dec, length=None, LittleEndian=False):#Converts a decimal into an ascii string of chosen length 47 | out = [] 48 | while dec <> 0: 49 | out.insert(0, dec&0xFF) 50 | dec >>= 8 51 | #"".join(map(chr, out)) 52 | 53 | if length: 54 | if len(out) > length: 55 | #return "".join(map(chr, out[-length:])) 56 | out = out[-length:] 57 | if len(out) < length: 58 | #return "".join(map(chr, [0]*(length-len(out)) + out)) 59 | out = [0]*(length-len(out)) + out 60 | 61 | if LittleEndian: out.reverse() 62 | return "".join(map(chr, out)) 63 | def AddPadding(i,pad = 0x10):#used mainly for zipaligning offsets 64 | if i % pad <> 0: 65 | return i + pad - (i % pad) 66 | return i 67 | 68 | #palettes: 69 | FramePalette = [0xFFFFFFFF,0x000000FF,0xFF0000FF,0x0000FFFF] 70 | ThumbPalette = (0xFEFEFEFF,#0 71 | 0x4F4F4FFF,#1 72 | 0xFFFFFFFF,#2 73 | 0x9F9F9FFF,#3 74 | 0xFF0000FF,#4 75 | 0x770000FF,#5 76 | 0xFF7777FF,#6 77 | 0x00FF00FF,#7- 78 | 0x0000FFFF,#8 79 | 0x000077FF,#9 80 | 0x7777FFFF,#A 81 | 0x00FF00FF,#B- 82 | 0xFF00FFFF,#C 83 | 0x00FF00FF,#D- 84 | 0x00FF00FF,#E- 85 | 0x00FF00FF)#F- 86 | 87 | #Class PPM: 88 | # 89 | # With this class you can read a Flipnote PPM file into memory and get its attributes. 90 | # You do so by using either instance.ReadFile() or instance.Read() of your instance of the PPM class. 91 | # When reading the PPM file you can specify what parts you need by passing these parameters: 92 | # - DecodeThumbnail = True: The thumbnail will be predecoded. GetThumbnail() will still work if set to False, only takes a bit longer. Default is False 93 | # - ReadFrames = True: The frames are decoded. Disabling this will speed up the PPM reading a lot. Default is True 94 | # - ReadSound = True: Reads the sound data. Default is False 95 | # 96 | # After reading a PPM file, these values are available: 97 | # 98 | # - instance.FrameCount: The number of frames this flipnote consists of 99 | # - instance.Locked: A boolean telling us wether it's locked or not 100 | # - instance.ThumbnailFrameIndex: Which frame the thumbnail is(starting at 0) 101 | # - instance.OriginalAuthorName: The name of the original author in unicode 102 | # - instance.EditorAuthorName: The name of the (previous?) editing author in unicode 103 | # - instance.Username: The username in unicode 104 | # - instance.OriginalAuthorID: The ID of the original author 105 | # - instance.EditorAuthorID: The ID of the last editor, the last user to save the file 106 | # - instance.PreviousEditAuthorID: The ID of the previous editor 107 | # - instance.OriginalFilename: The original filename 108 | # - instance.CurrentFilename: The current filename 109 | # - instance.Date: The date in seconds since January 1'st 2000 110 | # - instance.GetThumbnail(): The thumbnail stored in a 2D list of uint32 RGBA values (>u4). Will decode it first if not done so already 111 | # - instance.GetFrame(n): Returns the specified frame as a 2D list of uint32 RGBA values (>u4). Only works if ReadFrames was true when reading the PPM file 112 | # - instance.GetSound(n): Returns the decoded sound. Index 0 is the BGM. 1, 2 and 3 are the SFXs. if outputpath is None, the raw mono PCM @ 8184Hz is returned instead as a string 113 | # - instance.Looped: A boolean telling us wether it's looped or not 114 | # - instance.SFXUsage: A list telling us when the different sound effects are used: 115 | # instance.SFXUsage[frame] = [SFX1, SFX2, SFX3] where the SFX's are booleans 116 | # - instance.Framespeed: a value between 1 and 8 telling us the speed 117 | # - instance.BGMFramespeed: The framespeed when the BGM was recorded. used for resampling 118 | # - instance.Frames: A list containing the decoded frames' raw data. Only available if ReadFrames was true when reading the PPM file 119 | # instance.Frames[frame] = [Inverted(boolean), [color #1, color #2], frame] 120 | # color #1 and #2 is a value between 0 and 3, check the palette in instance.GetFrame() to see what they are. 121 | # frame is a 3d list, used like this. frame[layer][x][y] = boolean 122 | # To get a colored image of a frame, use instance.GetFrame() 123 | # - instance.SoundData: A list contaning the raw data of the BGM and the 3 sound effects. Only available if ReadSound was true set to true 124 | class PPM: 125 | def __init__(self,forced_speed=None): 126 | self.Loaded = [False, False, False]#(Meta, Frames, Sound) 127 | self.Frames = None 128 | self.Thumbnail = None 129 | self.RawThumbnail = None 130 | self.SoundData = None 131 | self.forced_speed = forced_speed 132 | def ReadFile(self, path, DecodeThumbnail=False, ReadFrames=True, ReadSound=False):#Load: (thumbnail, frames, sound) 133 | f = open(path, "rb") 134 | ret = self.Read(f.read(), DecodeThumbnail, ReadFrames, ReadSound) 135 | f.close() 136 | return ret 137 | def Read(self, data, DecodeThumbnail=False, ReadFrames=True, ReadSound=False):#Load: (thumbnail, frames, sound) 138 | if data[:4] <> "PARA" or len(data) <= 0x6a0: 139 | return False 140 | 141 | 142 | #Read the header: 143 | AudioOffset = AscDec(data[4:8], True) + 0x6a0 144 | AudioLenght = AscDec(data[8:12], True) 145 | 146 | self.FrameCount = AscDec(data[12:14], True) + 1 147 | self.Locked = ord(data[0x10]) & 0x01 == 1 148 | self.ThumbnailFrameIndex = AscDec(data[0x12:0x14], True)#which frame the thumbnnail represents 149 | 150 | self.OriginalAuthorName = data[0x14:0x2A].decode("UTF-16LE").split(u"\0")[0] 151 | self.EditorAuthorName = data[0x2A:0x40].decode("UTF-16LE").split(u"\0")[0] 152 | self.Username = data[0x40:0x56].decode("UTF-16LE").split(u"\0")[0] 153 | 154 | self.OriginalAuthorID = data[0x56:0x5e][::-1].encode("HEX").upper() 155 | self.EditorAuthorID = data[0x5E:0x66][::-1].encode("HEX").upper()#the last user to save the file 156 | 157 | self.OriginalFilenameC = data[0x66:0x78]#compressed 158 | self.CurrentFilenameC = data[0x78:0x8a]#compressed 159 | self.OriginalFilename = "%s_%s_%s.ppm" % (self.OriginalFilenameC[:3].encode("HEX").upper(), self.OriginalFilenameC[3:-2], str(AscDec(self.OriginalFilenameC[-2:], True)).zfill(3)) 160 | self.CurrentFilename = "%s_%s_%s.ppm" % (self.CurrentFilenameC[:3].encode("HEX").upper(), self.CurrentFilenameC[3:-2], str(AscDec(self.CurrentFilenameC[-2:], True)).zfill(3)) 161 | 162 | self.PreviousEditAuthorID = data[0x8a:0x92][::-1].encode("HEX").upper()#don't know what this really is 163 | 164 | #self.PartialFilenameC = data[0x92:0x9a]#compressed 165 | 166 | self.Date = AscDec(data[0x9a:0x9e], True)#in seconds since midnight 1'st january 2000 167 | 168 | self.RawThumbnail = data[0xa0:0x6a0] 169 | if DecodeThumbnail: 170 | self.GetThumbnail()#self.Thumbnail[x, y] = uint32 RGBA 171 | 172 | self.Loaded[0] = True 173 | 174 | #read the animation sequence header: 175 | self.Looped = ord(data[0x06A6]) >> 1 & 0x01 == 1#Unverified? 176 | AnimationOffset = 0x6a8 + AscDec(data[0x6a0:0x6a4], True) 177 | FrameOffsets = [AnimationOffset + AscDec(data[0x06a8+i*4:0x06a8+i*4+4], True) for i in xrange(self.FrameCount)] 178 | 179 | #Read the audio header: 180 | self.SFXUsage = [(i&0x1<>0, i&0x2<>0, i&0x4<>0) for i in map(ord, data[AudioOffset:AudioOffset+self.FrameCount])]#SFXUsage[frame] = (sfx1, sfx2, sfx2) shere sfxX is either 0 or 1 181 | SoundSize =(AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4) :AddPadding(AudioOffset+self.FrameCount, 4)+ 4], True),#BG music 182 | AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+ 4:AddPadding(AudioOffset+self.FrameCount, 4)+ 8], True),#SFX1 183 | AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+ 8:AddPadding(AudioOffset+self.FrameCount, 4)+12], True),#SFX2 184 | AscDec(data[AddPadding(AudioOffset+self.FrameCount, 4)+12:AddPadding(AudioOffset+self.FrameCount, 4)+16], True))#SFX3 185 | if self.forced_speed == None: 186 | self.Framespeed = 8 - ord(data[AddPadding(AudioOffset+self.FrameCount, 4) + 16]) 187 | else: 188 | self.Framespeed = self.forced_speed 189 | self.BGMFramespeed = 8 - ord(data[AddPadding(AudioOffset+self.FrameCount, 4) + 17])#framespeed when the bgm was recorded 190 | 191 | ## self.BGMFramespeed = self.forced_speed 192 | 193 | #Read the Frames: 194 | if ReadFrames: 195 | self.Frames = []#self.Frames[frame] = [inverted(bool), (color1(0-2), color2(0-2)), frame[layer, x, y] = bool] 196 | for i, offset in enumerate(FrameOffsets): 197 | #Read frame header: 198 | Inverted = ord(data[offset]) & 0x01 == 0 199 | 200 | #Reads which color that will be used: 201 | Colors = (ord(data[offset]) >> 1 & 0x03, 202 | ord(data[offset]) >> 3 & 0x03) 203 | 204 | Frame = self.ExtractFrame(data, offset, self.Frames[i-1][2] if i else None) 205 | 206 | self.Frames.append([Inverted, Colors, Frame]) 207 | 208 | self.Loaded[1] = True 209 | else: 210 | self.Loaded[1] = False 211 | self.Frames = None 212 | 213 | #Read the Audio: 214 | if ReadSound: 215 | self.SoundData = [] 216 | pos = AddPadding(AudioOffset+self.FrameCount+32, 4) 217 | for i in xrange(4): 218 | self.SoundData.append(data[pos:pos+SoundSize[i]]) 219 | pos += SoundSize[i] 220 | 221 | self.Loaded[2] = True 222 | else: 223 | self.Loaded[2] = False 224 | self.SoundData = None 225 | 226 | #return the results 227 | return self 228 | def WriteFile(self, path):#not implented 229 | pass 230 | #Extracting: 231 | def GetFrame(self, frame):#frame is the index in self.Frames. Only works if ReadFrames was True when reading the PPM file 232 | global FramePalette 233 | if not self.Loaded[1]: return None 234 | 235 | Inverted, Colors, Frame = self.Frames[frame] 236 | 237 | #Defines the palette: 238 | Palette = FramePalette[:] 239 | if Inverted: 240 | Palette[0], Palette[1] = Palette[1], Palette[0] 241 | Color1 = Palette[Colors[0]] 242 | Color2 = Palette[Colors[1]] 243 | 244 | out = np.zeros((256, 192), dtype=">u4") 245 | out[:] = Palette[0] 246 | out[Frame[1]] = Color2 247 | out[Frame[0]] = Color1 248 | 249 | return out 250 | def GetThumbnail(self, force=False): 251 | if (self.Thumbnail is None or force):# and self.RawThumbnail: 252 | global ThumbPalette 253 | if not self.RawThumbnail: 254 | return False 255 | 256 | out = np.zeros((64, 48), dtype=">u4") 257 | 258 | #speedup: 259 | palette = ThumbPalette 260 | 261 | #8x8 tiling: 262 | for ty in range(6): 263 | for tx in range(8): 264 | for y in range(8): 265 | for x in range(0,8,2): 266 | #two colors stored in each byte: 267 | byte = ord(self.RawThumbnail[(ty*512+tx*64+y*8+x)/2]) 268 | out[x+tx*8 , y+ty*8] = palette[byte & 0xF] 269 | out[x+tx*8+1, y+ty*8] = palette[byte >> 4] 270 | 271 | self.Thumbnail = out 272 | return self.Thumbnail 273 | def GetSound(self, index, outputpath=None):#index 0 is BGM. 1, 2 and 3 is SFX. if outputpath is None, the raw mono PCM @ 8184Hz is returned instead 274 | if self.Loaded[2]: 275 | if self.SoundData[index]: 276 | #reverse nibbles: 277 | data = [] 278 | for i in map(ord,self.SoundData[index]): 279 | data.append(chr((i&0xF)<< 4 | (i>>4))) 280 | data = "".join(data) 281 | 282 | #4bit ADPCM decode 283 | decoded = audioop.adpcm2lin(data, 2, None)[0] 284 | 285 | #write to wav: 286 | if outputpath: 287 | f = wave.open(outputpath, "wb") 288 | f.setnchannels(1) 289 | f.setsampwidth(2) 290 | f.setframerate(8192)#possibly 8184, but not a noticable difference anyway 291 | f.writeframes(decoded) 292 | #f.writeframes("".join(out)) 293 | f.close() 294 | 295 | return True 296 | else: 297 | return decoded 298 | else: 299 | return False 300 | #sub functions: (private) 301 | def ExtractFrame(self, data, offset, PrevFrame=None): 302 | #defines line encoding storage: 303 | #Enc1 = [0 for i in range(192)] 304 | #Enc2 = [0 for i in range(192)] 305 | #Defines the frame storage: 306 | #Color1Frame = [[0 for i in range(192)] for i in range(256)] 307 | #Color2Frame = [[0 for i in range(192)] for i in range(256)] 308 | 309 | Encoding = [[], []] 310 | #Frame = [[[False for _ in xrange(192)] for _ in xrange(256)] for _ in xrange(2)] 311 | Frame = np.zeros((2, 256, 192), dtype=np.bool_) 312 | 313 | #Read tags: 314 | NewFrame = ord(data[offset]) & 0x80 <> 0 315 | Unknown = ord(data[offset]) >> 5 & 0x03 316 | 317 | offset += 1 318 | 319 | #WIP - framemove: 320 | FrameMove = [0,0] 321 | if Unknown & 0x2:#doesn't work 100%... 322 | print "FrameMove at offset ",offset-1 323 | 324 | move_x = AscDec(data[offset+0:offset+1], True) 325 | move_y = AscDec(data[offset+1:offset+2], True) 326 | FrameMove[0] = move_x if move_x <= 127 else move_x-256 327 | FrameMove[1] = move_y if move_y <= 127 else move_y-256 328 | offset += 2 329 | elif Unknown: 330 | print "Unknown tags:",Unknown,"at offset ",offset-1 331 | 332 | 333 | #read the line encoding of the layers: 334 | for layer in xrange(2): 335 | for byte in map(ord, data[offset:offset+48]): 336 | Encoding[layer].append(byte & 0x03) 337 | Encoding[layer].append(byte >> 2 & 0x03) 338 | Encoding[layer].append(byte >> 4 & 0x03) 339 | Encoding[layer].append(byte >> 6 ) 340 | offset += 48 341 | 342 | #read layers: 343 | for layer in xrange(2): 344 | for y in xrange(192): 345 | if Encoding[layer][y] == 0:#Nothing 346 | pass 347 | elif Encoding[layer][y] == 1:#Normal 348 | UseByte = AscDec(data[offset:offset+4]) 349 | offset += 4 350 | x = 0 351 | while UseByte & 0xFFFFFFFF: 352 | if UseByte & 0x80000000: 353 | byte = ord(data[offset]) 354 | offset += 1 355 | for _ in xrange(8): 356 | if byte & 0x01: 357 | Frame[layer][x][y] = True 358 | x += 1 359 | byte >>= 1 360 | else: 361 | x += 8 362 | UseByte <<= 1 363 | elif Encoding[layer][y] == 2:#Inverted 364 | UseByte = AscDec(data[offset:offset+4]) 365 | offset += 4 366 | x = 0 367 | while UseByte&0xFFFFFFFF: 368 | if UseByte & 0x80000000: 369 | byte = ord(data[offset]) 370 | offset += 1 371 | for _ in range(8): 372 | if not byte & 0x01: 373 | Frame[layer, x, y] = True 374 | x += 1 375 | byte >>= 1 376 | else: 377 | x += 8 378 | UseByte <<= 1 379 | for n in range(256): 380 | #Frame[layer][n][y] = not Frame[layer][n][y] 381 | Frame[layer, n, y] = not Frame[layer, n, y] 382 | elif Encoding[layer][y] == 3:#Raw/full 383 | x = 0 384 | for _ in range(32): 385 | byte = ord(data[offset]) 386 | offset += 1 387 | for _ in range(8): 388 | if byte & 0x01: 389 | Frame[layer, x, y] = True 390 | x += 1 391 | byte >>= 1 392 | 393 | #Merges this frame with the previous frame if NewFrame isn't true: 394 | if not NewFrame and PrevFrame.all() <> None:#maybe optimize this better for numpy... 395 | if FrameMove[0] or FrameMove[1]:#Moves the previous frame if specified: 396 | NewPrevFrame = np.zeros((2, 256, 192), dtype=np.bool_) 397 | 398 | for y in range(192):#this still isn't perfected 399 | for x in range(256): 400 | TempX = x+FrameMove[0] 401 | TempY = y+FrameMove[1] 402 | if 0 <= TempX < 256 and 0 <= TempY < 192: 403 | NewPrevFrame[0, TempX, TempY] = PrevFrame[0, x, y] 404 | NewPrevFrame[1, TempX, TempY] = PrevFrame[1, x, y] 405 | 406 | PrevFrame = NewPrevFrame 407 | 408 | #merge the frames: 409 | Frame = Frame <> PrevFrame 410 | 411 | return Frame 412 | 413 | #Class TMB: 414 | # 415 | # With this class you can read a Flipnote TMB file into memory and get its attributes. 416 | # You do so by using either instance.ReadFile() or instance.Read() of your instance of the TMB class. 417 | # If you pass DecodeThumbnail=True to these functions, it will decode thumbnail. 418 | # The TMB file is a file containing the metadata to a corresponding PPM file. 419 | # 420 | # After reading a TMB file, these values are available: 421 | # 422 | # - instance.FrameCount: The number of frames this flipnote consists of 423 | # - instance.Locked: A boolean telling us wether it's locked or not 424 | # - instance.ThumbnailFrameIndex: Which frame the thumbnail is(starting at 0) 425 | # - instance.OriginalAuthorName: The name of the original author in unicode 426 | # - instance.EditorAuthorName: The name of the (previous?) editing author in unicode 427 | # - instance.Username: The username in unicode 428 | # - instance.OriginalAuthorID: The ID of the original author 429 | # - instance.EditorAuthorID: The ID of the last editor, the last user to save the file 430 | # - instance.PreviousEditAuthorID: The ID of the previous editor 431 | # - instance.OriginalFilename: The original filename 432 | # - instance.CurrentFilename: The current filename 433 | # - instance.Date: The date in seconds since January 1'st 2000 434 | # - instance.GetThumbnail(): The thumbnail stored in a 2D numpy array of uint32 RGBA values (>u4). Will decode it first if not done so already 435 | class TMB: 436 | def __init__(self): 437 | self.Loaded = False 438 | self.Thumbnail = None 439 | self.RawThumbnail = None 440 | def ReadFile(self, path, DecodeThumbnail=False): 441 | f = open(path, "rb") 442 | ret = self.Read(f.read(), DecodeThumbnail) 443 | f.close() 444 | return ret 445 | def Read(self, data, DecodeThumbnail=False): 446 | if data[:4] <> "PARA" or len(data) < 0x6a0: 447 | return False 448 | 449 | #Read the header: 450 | self.AudioOffset = AscDec(data[4:8], True) + 0x6a0#only stored for self.Pack() 451 | self.AudioLenght = AscDec(data[8:12], True)#only stored for self.Pack() 452 | 453 | self.FrameCount = AscDec(data[12:14], True) + 1 454 | self.Locked = ord(data[0x10]) & 0x01 == 1 455 | self.ThumbnailFrameIndex = AscDec(data[0x12:0x14], True)#which frame is in the thumbnnail 456 | 457 | #self.OriginalAuthorName = u"".join(unichr(AscDec(data[0x14+i*2:0x14+i*2+2], True)) for i in xrange(11)).split(u"\0")[0] 458 | #self.EditorAuthorName = u"".join(unichr(AscDec(data[0x2A+i*2:0x2A+i*2+2], True)) for i in xrange(11)).split(u"\0")[0] 459 | #self.Username = u"".join(unichr(AscDec(data[0x40+i*2:0x40+i*2+2], True)) for i in xrange(11)).split(u"\0")[0] 460 | self.OriginalAuthorName = data[0x14:0x2A].decode("UTF-16LE").split(u"\0")[0] 461 | self.EditorAuthorName = data[0x2A:0x40].decode("UTF-16LE").split(u"\0")[0] 462 | self.Username = data[0x40:0x56].decode("UTF-16LE").split(u"\0")[0] 463 | 464 | self.OriginalAuthorID = data[0x56:0x5e][::-1].encode("HEX").upper() 465 | self.EditorAuthorID = data[0x5E:0x66][::-1].encode("HEX").upper()#the last user to save the file 466 | 467 | self.OriginalFilenameC = data[0x66:0x78]#compressed 468 | self.CurrentFilenameC = data[0x78:0x8a]#compressed 469 | self.OriginalFilename = "%s_%s_%s.tmb" % (self.OriginalFilenameC[:3].encode("HEX").upper(), self.OriginalFilenameC[3:-2], str(AscDec(self.OriginalFilenameC[-2:], True)).zfill(3)) 470 | self.CurrentFilename = "%s_%s_%s.tmb" % (self.CurrentFilenameC[:3].encode("HEX").upper(), self.CurrentFilenameC[3:-2], str(AscDec(self.CurrentFilenameC[-2:], True)).zfill(3)) 471 | 472 | self.PreviousEditAuthorID = data[0x8a:0x92][::-1].encode("HEX").upper()#don't know what this really is 473 | 474 | self.PartialFilenameC = data[0x92:0x9a]#compressed 475 | 476 | self.Date = AscDec(data[0x9a:0x9e], True)#in seconds since midnight 1'st january 2000 477 | 478 | self.RawThumbnail = data[0xa0:0x6a0] 479 | if DecodeThumbnail: 480 | self.GetThumbnail()#self.Thumbnail[x, y] = uint32 RGBA 481 | 482 | self.Loaded = True 483 | #return the results 484 | return self 485 | def WriteFile(self, path):#not implented 486 | out = self.Pack() 487 | if out: 488 | f = open(path, "wb") 489 | f.write(out) 490 | f.close() 491 | return True 492 | else: 493 | return False 494 | def Pack(self, ppm=None):#not implented 495 | if not self.Loaded: return False 496 | 497 | #realself = self 498 | #if ppm: self = ppm 499 | 500 | out = ["PARA",#magic 501 | DecAsc(self.AudioOffset-0x6a0, 4, True),#animation data size 502 | DecAsc(self.AudioLenght, 4, True),#audio data size 503 | DecAsc(self.FrameCount-1, 2, True),#frame count 504 | "\x24\x00",#unknown 505 | chr(self.Locked), "\0",#locked 506 | DecAsc(self.ThumbnailFrameIndex, 2, True),#which frame is in the thumbnnail 507 | self.OriginalAuthorName.encode("UTF-16LE") + "\0\0"*(11-len(self.OriginalAuthorName)),#Original Author Name 508 | self.EditorAuthorName.encode("UTF-16LE") + "\0\0"*(11-len(self.EditorAuthorName)),#Editor Author Name 509 | self.Username.encode("UTF-16LE") + "\0\0"*(11-len(self.Username)),#Username 510 | self.OriginalAuthorID.decode("HEX")[::-1],#OriginalAuthorID 511 | self.EditorAuthorID.decode("HEX")[::-1],#EditorAuthorID 512 | self.OriginalFilenameC,#OriginalFilename 513 | self.CurrentFilenameC,#CurrentFilename 514 | self.PreviousEditAuthorID.decode("HEX")[::-1],#EditorAuthorID 515 | self.PartialFilenameC,#PartialFilename 516 | DecAsc(self.Date, 4, True),#Date in seconds 517 | "\0\0",#padding 518 | self.PackThumbnail()]#thumbnail 519 | 520 | return "".join(out) 521 | def GetThumbnail(self, force=False): 522 | if (self.Thumbnail is None or force):# and self.RawThumbnail: 523 | global ThumbPalette 524 | if not self.RawThumbnail: 525 | return False 526 | 527 | out = np.zeros((64, 48), dtype=">u4") 528 | 529 | #speedup: 530 | palette = ThumbPalette 531 | 532 | #8x8 tiling: 533 | for ty in range(6): 534 | for tx in range(8): 535 | for y in range(8): 536 | for x in range(0,8,2): 537 | #two colors stored in each byte: 538 | byte = ord(self.RawThumbnail[(ty*512+tx*64+y*8+x)/2]) 539 | out[x+tx*8 , y+ty*8] = palette[byte & 0xF] 540 | out[x+tx*8+1, y+ty*8] = palette[byte >> 4] 541 | 542 | self.Thumbnail = out 543 | return self.Thumbnail 544 | def PackThumbnail(self, Exact=True, force=False):#more or less a private function for now 545 | palette = (0xFEFEFEFF,#0 546 | 0x4F4F4FFF,#1 547 | 0xFFFFFFFF,#2 548 | 0x9F9F9FFF,#3 549 | 0xFF0000FF,#4 550 | 0x770000FF,#5 551 | 0xFF7777FF,#6 552 | 0x00FF00FF,#7- 553 | 0x0000FFFF,#8 554 | 0x000077FF,#9 555 | 0x7777FFFF,#A 556 | 0x00FF00FF,#B- 557 | 0xFF00FFFF,#C 558 | 0x00FF00FF,#D- 559 | 0x00FF00FF,#E- 560 | 0x00FF00FF)#F- 561 | 562 | 563 | if not self.Thumbnail: 564 | return self.RawThumbnail 565 | else: 566 | if Exact: 567 | out = [] 568 | 569 | #8x8 tiling: 570 | for ty in range(6): 571 | for tx in range(8): 572 | for y in range(8): 573 | for x in range(0,8,2): 574 | #two colors stored in each byte: 575 | #pos = 0xa0+(ty*512+tx*64+y*8+x)/2 576 | p1 = palette.index(int(self.Thumbnail[x+tx*8 , y+ty*8])) 577 | p2 = palette.index(int(self.Thumbnail[x+tx*8+1, y+ty*8])) 578 | out.append(chr(p2<<4 | p1)) 579 | 580 | self.RawThumbnail = "".join(out) 581 | return self.RawThumbnail 582 | else: 583 | #not yet implented 584 | return False 585 | 586 | #Function WriteImage: 587 | # 588 | # Writes a 2D numpy array of uint32 RGBA values (>u4) as a image files. 589 | # Designed to work with PPM().Thumbnail or PPM().GetFrame(n) 590 | # 591 | # This function requires the PIl imaging module 592 | def WriteImage(image, outputPath): 593 | if not hasPIL: 594 | print "Error: PIL not found!" 595 | return False 596 | #if not image: return False 597 | 598 | out = image.tostring("F") 599 | 600 | # out = [] 601 | # for y in xrange(len(image[0])): 602 | # for x in xrange(len(image)): 603 | # out.append(DecAsc(image[x][y], 4)) 604 | 605 | out = Image.frombytes("RGBA", (len(image), len(image[0])), out) 606 | 607 | filetype = outputPath[outputPath.rfind(".")+1:] 608 | out.save(outputPath, filetype) 609 | 610 | 611 | return True 612 | 613 | def get_metadata(flipnote): 614 | epoch = time.mktime(time.struct_time([2000, 1, 1, 0, 0, 0, 5, 1, -1])) 615 | 616 | meta = { 617 | u"Current filename":flipnote.CurrentFilename[:-3]+filetype, 618 | u"Original filename":flipnote.OriginalFilename[:-3]+filetype, 619 | u"Number of frames":flipnote.FrameCount, 620 | u"Locked":flipnote.Locked, 621 | u"Thumbnail frame index":(flipnote.ThumbnailFrameIndex+1), 622 | u"Original author":flipnote.OriginalAuthorName, 623 | u"Editor author":flipnote.EditorAuthorName, 624 | u"Username":flipnote.Username, 625 | u"Original author ID":flipnote.OriginalAuthorID, 626 | u"Editor author ID":flipnote.EditorAuthorID, 627 | u"Date(seconds since 1'st Jan 2000)":flipnote.Date, 628 | u"Date":time.strftime("%H:%M %d/%m-%Y (faulty)", time.gmtime(epoch+flipnote.Date)), 629 | } 630 | if filetype == "ppm": 631 | meta[u"Frame speed"]=flipnote.Framespeed 632 | meta[u"BGM Frame speed"]=flipnote.BGMFramespeed 633 | meta[u"Looped"]=flipnote.Looped 634 | 635 | return meta 636 | 637 | def DumpFrames(flipnote,directory): 638 | for i in xrange(flipnote.FrameCount): 639 | print "Dumping frame #%i..." % (i+1), 640 | WriteImage(flipnote.GetFrame(i), os.path.join(directory, "frame %s.png" % str(i+1).zfill(3))) 641 | 642 | def DumpSoundFiles(flipnote,directory,raw=False): 643 | for i, data in enumerate(flipnote.SoundData): 644 | if not data: continue 645 | path = os.path.join(directory, ("BGM.wav", "SFX1.wav", "SFX2.wav", "SFX3.wav")[i]) 646 | flipnote.GetSound(i, path) 647 | 648 | if raw: 649 | f = open(path[:-3]+"bin", "wb") 650 | f.write(data) 651 | f.close() 652 | 653 | def DumpSFXUsage(flipnote,directory): 654 | with open(os.path.join(directory, "SFX usage.txt"), "w") as f: 655 | for i, (s1, s2, s3) in enumerate(flipnote.SFXUsage): 656 | f.write("Frame %i:%s%s%s\n" % (i, " SFX1"*s1, " SFX2"*s2, " SFX3"*s3)) 657 | 658 | if __name__ == '__main__': 659 | print " == PPM.py ==" 660 | print " == by pbsds ==" 661 | print " == v1.3 ==" 662 | print 663 | 664 | if len(sys.argv) < 3: 665 | print "Usage:" 666 | print " PPM.py [] []" 667 | print "" 668 | print " :" 669 | print " -t: Extracts the thumbnail to the file " 670 | print " -f: Extracts the frame(s) to " 671 | print " -s: Dumps the sound files to the folder " 672 | print " -S: Same as mode -s, but will also dump the raw sound data files" 673 | print " -e: Exports the flipnote to an MKV" 674 | print " -m: Prints out the metadata. Can also write it to which also" 675 | print " supports unicode charactes." 676 | print " -oa: Seach a directory for an original author that matches the RegEx" 677 | print " Mode -t and -m supports TMB files aswell" 678 | print " " 679 | print " Only used in mode -f" 680 | print " Set this to the exact frame you want to extract(starting at 1) and it" 681 | print " will be saved as a file to ." 682 | print " If not specified, it will extract all frames to the folder " 683 | 684 | sys.exit() 685 | 686 | import os, time 687 | 688 | if sys.argv[1] == "-t": 689 | print "Reading the flipnote file...", 690 | if not os.path.isfile(sys.argv[2]): 691 | print "Error!\nSpecified file doesn't exist!" 692 | sys.exit() 693 | flipnote = TMB().ReadFile(sys.argv[2], True) 694 | if not flipnote: 695 | print "Error!" 696 | print "The given file is not a Flipnote PPM file or TMB file!" 697 | sys.exit() 698 | print "Done!" 699 | 700 | print "Dumping the thumbnail...", 701 | WriteImage(flipnote.GetThumbnail(), sys.argv[3]) 702 | print "Done!" 703 | elif sys.argv[1] == "-f": 704 | if len(sys.argv) < 4: 705 | print "Error!" 706 | print " not specified!" 707 | sys.exit() 708 | 709 | print "Reading the flipnote file...", 710 | if not os.path.isfile(sys.argv[2]): 711 | print "Error!\nSpecified file doesn't exist!" 712 | sys.exit() 713 | flipnote = PPM().ReadFile(sys.argv[2]) 714 | if not flipnote: 715 | print "Error!\nThe given file is not a Flipnote PPM file." 716 | sys.exit() 717 | print "Done!" 718 | 719 | 720 | if len(sys.argv) < 5: 721 | if not os.path.isdir(sys.argv[3]): 722 | print "Error!\nThe specified directory doesn't exist!" 723 | sys.exit() 724 | 725 | DumpFrames(flipnote,sys.argv[3]) 726 | 727 | print "Done!" 728 | else: 729 | try: 730 | int(sys.argv[4]) 731 | except: 732 | print "Error!\nInvalid !" 733 | sys.exit() 734 | 735 | if not (0 <= int(sys.argv[4])-1 < flipnote.FrameCount): 736 | print "Error!\n is out of bounds!" 737 | sys.exit() 738 | 739 | print "Dumping frame #%i..." % int(sys.argv[4]), 740 | WriteImage(flipnote.GetFrame(int(sys.argv[4])-1), sys.argv[3]) 741 | print "Done!" 742 | elif sys.argv[1] in ("-s", "-S"): 743 | if len(sys.argv) < 4: 744 | print "Error!" 745 | print " not specified!" 746 | sys.exit() 747 | 748 | print "Reading the flipnote file...", 749 | if not os.path.isfile(sys.argv[2]): 750 | print "Error!\nSpecified file doesn't exist!" 751 | sys.exit() 752 | flipnote = PPM().ReadFile(sys.argv[2], ReadFrames=False, ReadSound=True) 753 | if not flipnote: 754 | print "Error!\nThe given file is not a Flipnote PPM file." 755 | sys.exit() 756 | print "Done!" 757 | 758 | if not os.path.isdir(sys.argv[3]): 759 | print "Error!\nThe specified output directory doesn't exist!" 760 | sys.exit() 761 | 762 | print "Converting the sound files...", 763 | DumpSoundFiles(flipnote,sys.argv[3],raw=(sys.argv[1]=="-S")) 764 | print "Done!" 765 | 766 | print "Dumping the sound effect usage...", 767 | DumpSFXUsage(flipnote,sys.argv[3]) 768 | print "Done!" 769 | elif sys.argv[1] == "-m": 770 | if not os.path.isfile(sys.argv[2]): 771 | print "Error!\nSpecified file doesn't exist!" 772 | sys.exit() 773 | 774 | filetype = "ppm" if sys.argv[2][-3:] == "ppm" else "tmb" 775 | flipnote = TMB().ReadFile(sys.argv[2]) if filetype == "tmb" else PPM().ReadFile(sys.argv[2], ReadFrames=False) 776 | if not flipnote: 777 | print "Error!\nThe given file is not a Flipnote PPM file or TMB file." 778 | if len(sys.argv) >= 4: 779 | f = open(sys.argv, "wb") 780 | f.write("Error!\nThe given file is not a Flipnote PPM file or TMB file.") 781 | f.close() 782 | sys.exit() 783 | 784 | meta = get_metadata(flipnote) 785 | newline = "\n" 786 | if sys.platform in ("win32", "cygwin"): newline = "\r\n" 787 | elif sys.platform in ("darwin"): newline = "\r" 788 | 789 | print newline.join(["\t".join([unicode(char) for char in line]) for line in meta.items()]).encode('ascii', 'ignore') 790 | 791 | if len(sys.argv) >= 4: 792 | f = open(sys.argv[3], "wb") 793 | f.write(newline.join(meta).encode("UTF-8")) 794 | f.close() 795 | elif sys.argv[1] == "-oa": 796 | regex = re.compile(sys.argv[2]) 797 | os.chdir(sys.argv[3]) 798 | for filename in os.listdir("."): 799 | epoch = time.mktime(time.struct_time([2000, 1, 1, 0, 0, 0, 5, 1, -1])) 800 | if not os.path.isfile(filename): 801 | print "Error!\nSpecified file doesn't exist!" 802 | sys.exit() 803 | 804 | filetype = "ppm" if filename[-3:].lower() == "ppm" else "tmb" 805 | flipnote = TMB().ReadFile(filename) if filetype == "tmb" else PPM().ReadFile(filename, ReadFrames=False) 806 | if not flipnote: 807 | continue 808 | 809 | meta = get_metadata(flipnote) 810 | if regex.match(meta["Original author"]): 811 | print filename 812 | 813 | elif sys.argv[1] == "-e": 814 | if not hasffmpeg: 815 | print "Error!\nffmpeg is not installed." 816 | sys.exit() 817 | in_file = sys.argv[2] 818 | out_file = sys.argv[3] 819 | try: 820 | sleep_time = int(sys.argv[sys.argv.index("--sleep")+1]) 821 | except (IndexError,ValueError): 822 | sleep_time = 0 823 | print "Not sleeping." 824 | try: 825 | forced_speed = int(sys.argv[sys.argv.index("--speed")+1]) 826 | print "Using forced speed "+str(forced_speed) 827 | except (IndexError,ValueError): 828 | forced_speed = None 829 | 830 | if out_file.lower()[-4:] != ".mkv": 831 | out_file += ".mkv" 832 | if not os.path.isfile(in_file): 833 | print "Error!\nSpecified file doesn't exist!" 834 | sys.exit() 835 | if os.path.isfile(out_file): 836 | print "Overwrite existing file?" 837 | overwrite = "" 838 | while overwrite != "y" and overwrite != "n": 839 | overwrite = raw_input("(Y/N) ").lower() 840 | if overwrite == "n": 841 | print "Not overwriting; exiting." 842 | sys.exit() 843 | filetype = "ppm" if in_file[-3:].lower() == "ppm" else "tmb" 844 | flipnote = TMB().ReadFile(in_file) if filetype == "tmb" else PPM(forced_speed).ReadFile(in_file, ReadFrames=True, ReadSound=True) 845 | 846 | # Make temp dir and dump the frames and sound here 847 | tempdir = tempfile.mkdtemp() 848 | os.mkdir(tempdir+"/sounds") 849 | print "Dumping the frames..." 850 | DumpFrames(flipnote,tempdir) 851 | print "Done!" 852 | print "Dumping the sounds..." 853 | DumpSoundFiles(flipnote,tempdir+"/sounds") 854 | print "Done!" 855 | print "Dumping SFX usage..." 856 | DumpSFXUsage(flipnote,tempdir+"/sounds") 857 | print "Done!" 858 | 859 | # Now we need the metadata so we can look up the FPS 860 | SPEEDS = [None,0.5,1,2,4,6,12,20,30] 861 | print "Getting metadata..." 862 | metadata = get_metadata(flipnote) 863 | print "Done!" 864 | speed = int(metadata["Frame speed"]) 865 | fps = SPEEDS[speed] 866 | print "Flipnote is speed {speed}, so {fps} FPS".format(speed=speed,fps=fps) 867 | 868 | # Now to make the video in ffmpeg 869 | print "Exporting video with ffmpeg..." 870 | export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-i","{path}/sounds/BGM.wav".format(path=tempdir),"-c:v","libx264","-preset","veryslow","-c:a","pcm_s16le","-shortest","-y",out_file] 871 | if not os.path.isfile(tempdir+"/sounds/BGM.wav"): 872 | print "No background music. Adding silent track..." 873 | #has_bgm = False 874 | export_command = ["ffmpeg","-framerate",str(fps),"-start_number","1","-i","{path}/frame %03d.png".format(path=tempdir),"-f","lavfi","-i","anullsrc=r=8192:cl=mono","-c:v","libx264","-preset","veryslow","-c:a","pcm_s16le","-shortest","-y",out_file] 875 | else: 876 | #has_bgm = True 877 | pass 878 | with open(os.devnull) as null: 879 | subprocess.call(export_command,stdout=null,stderr=null) 880 | print "Done!" 881 | 882 | # If the audio has been sped up, we have to do it again manually 883 | bgm_speed = int(metadata["BGM Frame speed"]) 884 | if bgm_speed != speed: 885 | print "Background music speed must be modified!" 886 | original_rate = 8192 887 | newrate = 8192*(float(fps)/SPEEDS[bgm_speed]) 888 | print "Using new rate: "+str(newrate) 889 | speed_change_command = ["ffmpeg","-i","{path}/sounds/BGM.wav".format(path=tempdir),"-filter_complex","asetrate={rate}".format(rate=newrate),"-i",out_file,"-map","0:a","-map","1:v","-vcodec","copy","-acodec","pcm_s16le","{path}/temp_out.mkv".format(path=tempdir)] 890 | with open(os.devnull,"w") as null: 891 | subprocess.call(speed_change_command,stdout=null,stderr=null) 892 | os.remove(out_file) 893 | shutil.move("{path}/temp_out.mkv".format(path=tempdir),out_file) 894 | print "Done!" 895 | 896 | # These are the ffmpeg commands I need for each sound effect 897 | # The first generates a silent track, with variable length. This gets concatenated to the front of the sound effect. 898 | silence_command = ["ffmpeg","-f","lavfi","-i","anullsrc=r=8192:cl=mono","-t","{length}","-f","wav","-y","{path}/silence.wav"] 899 | # The second concatenates the silent track and the sound effect, producing a sound file that can be mixed into the video's audio track so that it plays at the correct time. 900 | SFX_command = ["ffmpeg","-i","{path}/silence.wav","-i","{path}/sounds/{sfx}.wav","-filter_complex","[0:a] [1:a] concat=n=2:v=0:a=1","-y","{path}/sfx.wav"] 901 | # The third mixes the silence+sound effect into the video file 902 | merge_command = ["ffmpeg","-i",out_file,"-i","{path}/sfx.wav","-filter_complex","[0:a][1:a] amix=inputs=2:duration=longest:dropout_transition={video_length},volume=2","-c:a","pcm_s16le","-c:v","copy","-max_muxing_queue_size","1024","-y","{path}/temp_out.mkv"] 903 | 904 | ## normalise_command = ["ffmpeg","-i",out_file,"-filter_complex","dynaudnorm","{path}/temp_out.mkv".format(path=tempdir)] 905 | ## for sfx in ["SFX1","SFX2","SFX3"]: 906 | ## if not os.path.isfile("{path}/sounds/{sfx}.wav".format(path=tempdir,sfx=sfx)): 907 | ## print sfx+" does not exist." 908 | ## continue 909 | ## else: 910 | 911 | # Read in the sound effect usage data 912 | print "Reading sound effect usage..." 913 | with open("{path}/sounds/SFX usage.txt".format(path=tempdir),"r") as sfx_usage_file: 914 | sfx_usage = sfx_usage_file.read().split("\n") 915 | print "Done!" 916 | 917 | # Iterate through the frames, checking if sound effects need to be added 918 | for frame in range(len(sfx_usage)): 919 | line = sfx_usage[frame] 920 | # If a frame has an associated sound effect, get which sound effect to use 921 | sfx = line.split(":")[1].strip() if line.strip() != "" else "" 922 | if sfx != "": # If a sound effect must be played... 923 | length = frame/float(fps) 924 | print "Adding "+sfx+" at {length} seconds into the video.".format(length=length) 925 | # ...run each command in series with the correct arguments 926 | with open(os.devnull,"w") as null: 927 | subprocess.call([i.format(path=tempdir,sfx=sfx,length=length) for i in silence_command],stdout=null,stderr=null) 928 | subprocess.call([i.format(path=tempdir,sfx=sfx) for i in SFX_command],stdout=null,stderr=null) 929 | subprocess.call([i.format(path=tempdir,video_length=fps*len(sfx_usage)) for i in merge_command],stdout=null,stderr=null) 930 | os.remove(out_file) 931 | shutil.move("{path}/temp_out.mkv".format(path=tempdir),out_file) 932 | print "Done!" 933 | time.sleep(sleep_time) # optional sleep -- in case you want to slow things down for HDD strain or reliability or something 934 | 935 | ## subprocess.call(normalise_command) 936 | ## os.remove(out_file) 937 | ## shutil.move("{path}/temp_out.mkv".format(path=tempdir),out_file) 938 | 939 | 940 | # Remove the temp dir and all files in it 941 | print "Removing temporary directory..." 942 | shutil.rmtree(tempdir) 943 | print "Done!" 944 | 945 | else: 946 | print "Error!\nThere's no such mode." 947 | -------------------------------------------------------------------------------- /Hatenatools/ReadMe.txt: -------------------------------------------------------------------------------- 1 | == Hatenatools == 2 | == By pbsds == 3 | == aka Peder Bergebakken Sundt == 4 | 5 | with video-encode additions by WORD559 6 | 7 | --VERSIONS-- 8 | PPM.py: v1.5 9 | UGO.py: v0.93 10 | NTFT.py: v0.95 11 | 12 | --DESCRIPTION-- 13 | PPM.py can read the metadata, thumbnail, frames and the sound of Flipnote Studio files: .ppm 14 | It can also handle .tmb files. 15 | 16 | UGO.py can read and write UGO files, and modify their content. It can also convert UGO files 17 | to and from the XML format 18 | 19 | NTFT.py can read ntft image files and convert them to PIL supported image files 20 | 21 | These are all tested and written in python 2.7 22 | These are all designed to be usable both as python modules and as stand alone applications. 23 | NTFT.py and PPM.py requires Numpy and PIL 24 | 25 | --DOCUMENTATION-- 26 | Documentation on the formats can be found here: 27 | https://github.com/pbsds/hatena-server/wiki 28 | 29 | --LICENSE-- 30 | Hatenatools is licensed under AGPL3 31 | See License.txt or http://www.gnu.org/licenses/agpl-3.0.html for more information 32 | 33 | --CREDIT-- 34 | -Steven for most of the documentation on DSiBrew and his frame decoding example on his talkpage 35 | -Remark for helping me understanding the 8x8 tiling on the preview images. 36 | -JSAfive for supplying .tmb and .ugo files 37 | -Austin Burk, Midmad on hatena haiku and WDLmaster on hcs64.com for determining the sound codec 38 | -jaames, for proper p-frame translation 39 | -------------------------------------------------------------------------------- /Hatenatools/UGO.py: -------------------------------------------------------------------------------- 1 | #UGO.py by pbsds 2 | #AGPL3 licensed 3 | # 4 | #This class reads and writes UGO files. 5 | #It can also export and import it in a xml format 6 | #Extended functionality added by PPM.py 7 | # 8 | #Credits: 9 | # 10 | # -Jsafive for supplying .ugo files 11 | # 12 | #Note: 13 | # The current implentation can possibly mess up the files stored in Section #2, but will do for most needs 14 | # Next version will probably bring a big change in the ugoxml format 15 | # 16 | import sys, os 17 | from base64 import b64encode, b64decode 18 | import xml.etree.ElementTree as ET 19 | 20 | try: 21 | import PPM 22 | HasPPM = True 23 | except ImportError: 24 | HasPPM = False 25 | 26 | #helpers: 27 | def AscDec(ascii, LittleEndian=False):#Converts a ascii string into a decimal 28 | ret = 0 29 | l = map(ord, ascii) 30 | if LittleEndian: l.reverse() 31 | for i in l: 32 | ret = (ret<<8) | i 33 | return ret 34 | def DecAsc(dec, length=None, LittleEndian=False):#Converts a decimal into an ascii string of chosen length 35 | out = [] 36 | while dec <> 0: 37 | out.insert(0, dec&0xFF) 38 | dec >>= 8 39 | #"".join(map(chr, out)) 40 | 41 | if length: 42 | if len(out) > length: 43 | #return "".join(map(chr, out[-length:])) 44 | out = out[-length:] 45 | if len(out) < length: 46 | #return "".join(map(chr, [0]*(length-len(out)) + out)) 47 | out = [0]*(length-len(out)) + out 48 | 49 | if LittleEndian: out.reverse() 50 | return "".join(map(chr, out)) 51 | def zipalign(length, r=4): 52 | return length + (4 - length % r) if length % r else length 53 | def indentXML(elem, level=0):#"borrowed" from: http://effbot.org/zone/element-lib.htm#prettyprint 54 | i = "\n" + level*"\t" 55 | if len(elem): 56 | if not elem.text or not elem.text.strip(): 57 | elem.text = i + "\t" 58 | if not elem.tail or not elem.tail.strip(): 59 | elem.tail = i 60 | for elem in elem: 61 | indentXML(elem, level+1) 62 | if not elem.tail or not elem.tail.strip(): 63 | elem.tail = i 64 | else: 65 | if level and (not elem.tail or not elem.tail.strip()): 66 | elem.tail = i 67 | 68 | #class UGO: 69 | class UGO: 70 | def __init__(self): 71 | self.Loaded = False 72 | def ReadFile(self, path): 73 | f = open(path, "rb") 74 | ret = self.Read(f.read()) 75 | f.close() 76 | return ret 77 | def Read(self, data): 78 | #Filestructure: 79 | # - Header 80 | # - Table of Contents 81 | # - Padding, normally of length 2 82 | # - Extra Data 83 | # - Padding 84 | #The padding seems to do zipaligning to a length of 4 85 | global HasPPM 86 | 87 | #Header: 88 | if data[:4] <> "UGAR": return False#The file isn't a UGO file 89 | Sections = AscDec(data[ 4: 8], True)#could also be version 90 | if Sections >= 1: 91 | self.TableLength = AscDec(data[ 8:12], True) 92 | if Sections >= 2: 93 | self.ExtraLength = AscDec(data[12:16], True) 94 | if Sections > 2: 95 | print "Warning: This UGO file has more than the 2 known sections:",Sections 96 | print "Please send this UGO file to pbsds over at pbsds.net" 97 | print "This file could possibly be read incorrectly..." 98 | headerlength = 8 + Sections*4 99 | 100 | #Read table of contents: 101 | #A table where the rows are seperated with newlines, and colons with tabs 102 | if Sections >= 1: 103 | TableOfContents = tuple((i.split("\t") for i in data[headerlength:headerlength+self.TableLength].split("\n"))) 104 | else: 105 | TableOfContents = [] 106 | 107 | #Extra data: 108 | if Sections >= 2: 109 | ExtraData = data[zipalign(headerlength+self.TableLength) : zipalign(headerlength+self.TableLength)+self.ExtraLength] 110 | 111 | #Parse data: 112 | self.Items = [] 113 | 114 | #todo: implement this: 115 | self.Files = []#[i] = (filename, filecontent) 116 | #see todo.txt 117 | 118 | pos = 0#Extra Data position 119 | tmbcount = 1#used if HasPPM is False 120 | ntftcount = 1#used if no label 121 | names = [] 122 | for i in TableOfContents: 123 | type = int(i[0]) 124 | if type == 0:#layout 125 | #I've always seen just numbers here. 126 | #The amount of numbers also differ. 127 | #maybe related to color scheme? or the button scheme/layout 128 | 129 | #all pages containing TMBs have so far been: i = ["0", "2", "1"] 130 | self.Items.append(("layout", map(int, i[1:]))) 131 | continue 132 | elif type == 1:#Text on topscreen 133 | num = int(i[1])#always seen as 0, unknown purpose 134 | labels = [b64decode(i[n]).decode("UTF-16LE") for n in xrange(2,7)]#5 labels probably one label for each line of text 135 | 136 | self.Items.append(("topscreen text", labels, num)) 137 | continue 138 | elif type == 2:#catogories(like "reccomended" and "new flipnotes" and "most popular") 139 | #this one may visually change greatly depending on what layout is set in type==0 140 | 141 | link = i[1] 142 | label = b64decode(i[2]).decode("UTF-16LE") 143 | selected = int(i[3]) <> 0#bool 144 | 145 | self.Items.append(("category", link, label, selected)) 146 | continue 147 | elif type == 3:#POST button/link to POST form. "Post Flipnote" uses this 148 | link = i[1] 149 | label = b64decode(i[2]).decode("UTF-16LE") 150 | 151 | self.Items.append(("post", link, label)) 152 | continue 153 | elif type == 4:#Button 154 | link = i[1] 155 | trait = int(i[2]) 156 | label = b64decode(i[3]).decode("UTF-16LE") 157 | other = i[4:]#varies 158 | 159 | #extra data 160 | file = None#== ("filename", "filedata") 161 | if trait < 100 and ExtraData: 162 | if ExtraData == "\x20":#empty 163 | pass#no files 164 | elif ExtraData[pos:pos+4] == "PARA":#tmb file 165 | file = ExtraData[pos:pos+0x6A0] 166 | pos += 0x6A0 167 | 168 | if HasPPM: 169 | tmb = PPM.TMB().Read(file) 170 | name = tmb.CurrentFilename[:-4] 171 | del tmb 172 | else: 173 | name = "embedded tmb #%i" % tmbcount 174 | tmbcount += 1 175 | 176 | if name+".tmb" in names: 177 | j = 2 178 | while "%s_%i.tmb" % (name, j) in names: 179 | j += 1 180 | name = "%s_%i" % (name, j) 181 | 182 | file = (name+".tmb", file) 183 | names.append(name+".tmb") 184 | else:#ntft icon 185 | name = label.encode("ascii", "ignore") 186 | if not name: 187 | name = "nameless ntft %i" % ntftcount 188 | ntftcount += 1 189 | 190 | if name+".ntft" in names: 191 | j = 2 192 | while "%s_%i.ntft" % (name, j) in names: 193 | j += 1 194 | name = "%s_%i" % (name, j) 195 | 196 | file = (name+".ntft", ExtraData[pos:pos+2048]) 197 | names.append(name+".ntft") 198 | pos += 2048 199 | 200 | self.Items.append(("button", trait, label, link, other, file)) 201 | 202 | 203 | # if subtype == 3:#flipnote 204 | # tmb = PPM.TMB().Read(ExtraData[pos:pos+0x6A0]); pos += 0x6A0 205 | # unknown1 = i[3]#empty 206 | # stars = int(i[4])#not sure 207 | # unknown2 = map(int, i[5:8])#unknown = [765, 573, 0] 208 | 209 | # self.Items.append(("flipnote", link, tmb, stars, unknown1, unknown2)) 210 | # continue 211 | # elif subtype == 100: pass 212 | # elif subtype == 101: pass 213 | # elif subtype == 102: pass 214 | # elif subtype == 104:#list item? like mails and announcements 215 | # label = b64decode(i[3]).decode("UTF-16LE") 216 | # unknown = i[4] 217 | # num = int(i[5])#only seen as 0 218 | 219 | # # self.Items.append(("list item?", link, label, unknown, num)) 220 | # # continue 221 | # pass 222 | # elif subtype == 115:#Labeled button link(size of a flipnote thumbnail, commonly "next page") 223 | # label = b64decode(i[3]).decode("UTF-16LE") 224 | # unknown = i[4:6] 225 | # self.Items.append(("thumbnail link", link, label, unknown)) 226 | # continue 227 | # elif subtype == 117: pass 228 | continue 229 | 230 | #if not recognized: 231 | self.Items.append(("unknown", i)) 232 | print "Unknown UGO item discovered:", i 233 | self.Loaded = True 234 | return self 235 | def WriteFile(self, path): 236 | if self.Loaded: 237 | out = self.Pack() 238 | if out: 239 | f = open(path, "wb") 240 | f.write(out) 241 | f.close() 242 | return True 243 | else: 244 | return False 245 | def Pack(self): 246 | if not self.Loaded: return False 247 | 248 | Header = ["UGAR", None] 249 | TableOfContents = [] 250 | ExtraData = [] 251 | 252 | #Encode data: 253 | for i in self.Items: 254 | if i[0] == "unknown": 255 | TableOfContents.append("\t".join(i[1])) 256 | elif i[0] == "layout":#0 257 | TableOfContents.append("\t".join(["0"] + map(str, list(i[1])))) 258 | elif i[0] == "topscreen text":#1 259 | labels, num = i[1:] 260 | 261 | num = str(num) 262 | for i in xrange(5): 263 | labels[i] = b64encode(labels[i].encode("UTF-16LE")) 264 | 265 | TableOfContents.append("\t".join(("1", num, labels[0], labels[1], labels[2], labels[3], labels[4]))) 266 | elif i[0] == "category":#2 267 | link, label, selected = i[1:] 268 | 269 | label = b64encode(label.encode("UTF-16LE")) 270 | selected = str(1*selected) 271 | 272 | TableOfContents.append("\t".join(("2", link, label, selected))) 273 | elif i[0] == "post":#3 274 | link, label = i[1:] 275 | 276 | label = b64encode(label.encode("UTF-16LE")) 277 | 278 | TableOfContents.append("\t".join(("3", link, label))) 279 | elif i[0] == "button":#4 280 | trait, label, link, other, file = i[1:] 281 | 282 | trait = str(trait) 283 | label = b64encode(label.encode("UTF-16LE")) 284 | 285 | TableOfContents.append("\t".join(["4", link, trait, label] + list(other))) 286 | 287 | if file: 288 | ExtraData.append(file[1]) 289 | else: 290 | print "Unrecognized entry in self.Items:", i 291 | TableOfContents = "\n".join(TableOfContents) 292 | ExtraData = "".join(ExtraData) 293 | 294 | #Format data: 295 | Sections = 0 296 | if TableOfContents: 297 | Sections += 1 298 | Header.append(DecAsc(len(TableOfContents), 4, True)) 299 | 300 | #padding/zipaligning 301 | if len(TableOfContents) % 4: 302 | TableOfContents += "\0" * (4 - len(TableOfContents) % 4) 303 | if ExtraData: 304 | Sections += 1 305 | Header.append(DecAsc(len(ExtraData), 4, True)) 306 | 307 | #padding/zipaligning 308 | if len(ExtraData) % 4: 309 | ExtraData += "\0" * (4 - len(ExtraData) % 4) 310 | Header[1] = DecAsc(Sections, 4, True) 311 | 312 | #Zip up and send the file: 313 | return "".join(Header) + TableOfContents + ExtraData 314 | #XML 315 | def WriteXML(self, xmlname="content.ugoxml", folder="content.ugoxml embedded"):#WIP 316 | if not self.Loaded: return False 317 | 318 | path, xmlname = os.path.split(xmlname) 319 | ugo_xml = ET.Element("ugo_xml") 320 | files = [] 321 | 322 | for i in self.Items: 323 | if i[0] == "unknown": 324 | elem = ET.SubElement(ugo_xml, "raw", type=i[1][0]) 325 | for value in i[1][1:]: 326 | ET.SubElement(elem, "value").text = value 327 | continue 328 | elif i[0] == "layout":#0 329 | elem = ET.SubElement(ugo_xml, "layout") 330 | for value in i[1]: 331 | ET.SubElement(elem, "value").text = str(value) 332 | continue 333 | elif i[0] == "topscreen text":#1 334 | elem = ET.SubElement(ugo_xml, "title") 335 | labels, num = i[1:] 336 | 337 | for label in labels: 338 | ET.SubElement(elem, "label").text = label 339 | 340 | ET.SubElement(elem, "num").text = str(num) 341 | continue 342 | elif i[0] == "category":#2 343 | elem = ET.SubElement(ugo_xml, "category") 344 | link, label, selected = i[1:] 345 | 346 | ET.SubElement(elem, "label").text = label 347 | ET.SubElement(elem, "address").text = link 348 | ET.SubElement(elem, "selected").text = str(selected).lower() 349 | elif i[0] == "post":#3 350 | elem = ET.SubElement(ugo_xml, "post") 351 | link, label = i[1:] 352 | 353 | ET.SubElement(elem, "label").text = label 354 | ET.SubElement(elem, "address").text = link 355 | elif i[0] == "button":#4 356 | elem = ET.SubElement(ugo_xml, "button") 357 | trait, label, link, other, file = i[1:] 358 | 359 | ET.SubElement(elem, "label").text = label 360 | ET.SubElement(elem, "address").text = link 361 | ET.SubElement(elem, "trait").text = str(trait)#todo: add names 362 | 363 | for n, value in enumerate(other): 364 | entry = ET.SubElement(elem, "value") 365 | entry.text = value 366 | if n == 0 and trait == 3: 367 | entry.attrib["tip"] = "stars" 368 | 369 | if file: 370 | ET.SubElement(elem, "embedded_file").text = os.path.join(folder, file[0]) 371 | files.append((os.path.join(folder, file[0]), file[1])) 372 | 373 | #intend 374 | indentXML(ugo_xml) 375 | 376 | #pack xml 377 | files.append((xmlname, ET.tostring(ugo_xml, encoding="UTF-8"))) 378 | 379 | #write files 380 | if not os.path.isdir(os.path.join(path, folder)): 381 | os.mkdir(os.path.join(path, folder)) 382 | for name, data in files: 383 | f = open(os.path.join(path, name), "wb") 384 | f.write(data) 385 | f.close() 386 | def ReadXML(self, xmlfile, silent=True): 387 | ugo_xml = ET.parse(xmlfile).getroot() 388 | xmlpath = os.path.split(xmlfile)[0] 389 | 390 | Items = [] 391 | for elem in ugo_xml: 392 | if elem.tag == "raw": 393 | if "type" not in elem.attrib: 394 | if not silent: print "Invalid formatting. without \"type\" attribute" 395 | return False 396 | values = [elem.attrib["type"]] 397 | for value in elem: 398 | if value.tag <> "value": 399 | if not silent: print "Invalid formatting. <%s> found within " % value.tag 400 | return False 401 | values.append(value.text if value.text else "") 402 | 403 | Items.append(("unknown", values)) 404 | elif elem.tag == "layout":#0 405 | values = [] 406 | for value in elem: 407 | if value.tag <> "value": 408 | if not silent: print "Invalid formatting. <%s> found within " % value.tag 409 | return False 410 | if not value.text.isdigit(): 411 | if not silent: print "Invalid entry. in is not a number" % value.tag 412 | return False 413 | values.append(int(value.text)) 414 | Items.append(("layout", values)) 415 | elif elem.tag == "title":#1 416 | labels = ["", "", "", "", ""] 417 | num = 0 418 | pos = 0 419 | numset = False 420 | 421 | for value in elem: 422 | if value.tag not in ("label", "num"): 423 | if not silent: print "Invalid formatting. <%s> found within " % value.tag 424 | return False 425 | if value.tag == "label": 426 | if pos >= 5: 427 | if not silent: print "Invalid formatting. More than 5 <labels> in <title>" 428 | return False 429 | if value.text: labels[pos] = value.text 430 | pos += 1 431 | elif value.tag == "num": 432 | if numset: 433 | if not silent: print "Invalid formatting. Multible <num> in <title>" 434 | return False 435 | if not value.text.isdigit(): 436 | if not silent: print "Invalid entry. <num> in <title> is not a number!" 437 | return False 438 | num = int(value.text) 439 | numset = True 440 | 441 | Items.append(("topscreen text", labels, num)) 442 | elif elem.tag == "category":#2 443 | link = None 444 | label = None 445 | selected = None 446 | 447 | for value in elem: 448 | if value.tag not in ("label", "address", "selected"): 449 | if not silent: print "Invalid formatting. <%s> found within <category>" % value.tag 450 | return False 451 | 452 | if value.tag == "address": 453 | if isinstance(link, str): 454 | if not silent: print "Invalid formatting. multible <address> within <category>" 455 | return False 456 | link = value.text if value.text else "" 457 | elif value.tag == "label": 458 | if isinstance(label, str): 459 | if not silent: print "Invalid formatting. multible <label> within <category>" 460 | return False 461 | label = value.text if value.text else "" 462 | elif value.tag == "selected": 463 | if selected in (True, False): 464 | if not silent: print "Invalid formatting. multible <selected> within <category>" 465 | return False 466 | selected = value.text[0].lower() in "t1" 467 | 468 | Items.append(("category", link, label, selected)) 469 | elif elem.tag == "post":#3 470 | label = None 471 | link = None 472 | 473 | for value in elem: 474 | if value.tag == "label": 475 | if isinstance(label, str): 476 | if not silent: print "Invalid formatting. Multible <label> within <post>" 477 | return False 478 | label = value.text if value.text else "" 479 | elif value.tag == "address": 480 | if isinstance(link, str): 481 | if not silent: print "Invalid formatting. Multible <address> within <post>" 482 | return False 483 | link = value.text if value.text else "" 484 | 485 | if None in (link, label): 486 | if not silent: print "Invalid formatting. <button> lacks either a <address> or <label>" 487 | return False 488 | 489 | Items.append(("post", link, label)) 490 | elif elem.tag == "button":#4 491 | trait = None#todo: add names 492 | label = None 493 | link = None 494 | other = [] 495 | file = None 496 | 497 | for value in elem: 498 | if value.tag not in ("label", "address", "trait", "value", "embedded_file"): 499 | if not silent: print "Invalid formatting. <%s> found within <button>" % value.tag 500 | return False 501 | 502 | if value.tag == "label": 503 | if isinstance(label, str): 504 | if not silent: print "Invalid formatting. Multible <label> within <button>" 505 | return False 506 | label = value.text if value.text else "" 507 | elif value.tag == "address": 508 | if isinstance(link, str): 509 | if not silent: print "Invalid formatting. Multible <address> within <button>" 510 | return False 511 | link = value.text if value.text else "" 512 | elif value.tag == "trait":#todo: add names 513 | if isinstance(trait, str): 514 | if not silent: print "Invalid formatting. Multible <trait> within <button>" 515 | return False 516 | if not value.text.isdigit(): 517 | if not silent: print "Invalid entry. <trait> in <button> is not a number" 518 | return False 519 | trait = int(value.text) 520 | elif value.tag == "value": 521 | other.append(value.text if value.text else "") 522 | elif value.tag == "embedded_file": 523 | if file <> None: 524 | if not silent: print "Invalid formatting. Multible <embedded_file> within <button>" 525 | return False 526 | 527 | path = os.path.join(xmlpath, value.text) 528 | if not os.path.isfile(path): 529 | if not silent: print "Invalid entry. Embedded file \"%s\" not found!" % value.text 530 | print path 531 | return False 532 | 533 | 534 | f = open(path, "rb") 535 | file = (os.path.split(value.text)[1], f.read()) 536 | f.close() 537 | 538 | if None in (trait, label, link): 539 | if not silent: print "Invalid formatting. <button> lacks either a <trait>, a <address> or a <label>" 540 | return False 541 | 542 | Items.append(("button", trait, label, link, other, file)) 543 | else: 544 | if not silent: 545 | print "Invalid formatting: <%s> found within <ugo_xml>" % elem.tag 546 | 547 | self.Items = Items 548 | self.Loaded = True 549 | return self 550 | 551 | if __name__ == "__main__": 552 | print " == UGO.py ==" 553 | print " == by pbsds ==" 554 | print " == v0.93 ==" 555 | print 556 | 557 | if len(sys.argv) < 2: 558 | print "Usage:" 559 | print " UGO.py [<mode>] <input> [<output> [<foldername>]]" 560 | print "" 561 | print " <Mode>:" 562 | print " -d: Converts the UGO file in <input> to a UGOXML file with the same" 563 | print " name, unless <output> is specified. Any embedded files will be" 564 | print " written to a folder called UGOXML-filename + \" embedded\" unless" 565 | print " <foldername> is given." 566 | print " <foldername> is relative to the XML." 567 | print " -e: Converts the UGOXML file in <input> to a UGO file with the same" 568 | print " name, unless <output> is specified." 569 | print " If mode is not specified, it will try to find out for itself" 570 | sys.exit() 571 | 572 | mode = sys.argv[1] 573 | if mode not in ("-d", "-e"): 574 | if os.path.exists(mode):#the mode is actually the file 575 | f = open(mode, "rb") 576 | magic = f.read(4) 577 | f.close() 578 | 579 | if magic == "UGAR": 580 | mode = "-d" 581 | print "No mode specified. UGO -> UGOXML chosen" 582 | else: 583 | mode = "-e" 584 | print "No mode specified. UGOXML -> UGO chosen" 585 | 586 | sys.argv.insert(1, mode) 587 | else: 588 | print "Invalid <mode> given!" 589 | sys.exit() 590 | 591 | if mode == "-d": 592 | input = sys.argv[2] 593 | output = sys.argv[3] if len(sys.argv) >= 4 else sys.argv[2]+"xml" 594 | foldername = sys.argv[4] if len(sys.argv) >= 5 else os.path.split(output)[1] + " embedded" 595 | 596 | print "Reading %s..." % os.path.split(input)[1] 597 | ugo = UGO().ReadFile(input) 598 | if not ugo: 599 | print "Error!\n The given file is not a UGO file!" 600 | sys.exit() 601 | print "Done!" 602 | 603 | print "Writing XML..." 604 | ugo.WriteXML(output, foldername) 605 | 606 | print "Done!\n\nHave a nice day!" 607 | if mode == "-e": 608 | input = sys.argv[2] 609 | output = sys.argv[3] if len(sys.argv) >= 4 else ".".join(sys.argv[2].split(".")[:-1]) + ".ugo" 610 | 611 | print "Reading %s..." % os.path.split(input)[1] 612 | try: 613 | ugo = UGO().ReadXML(input, False) 614 | except EL.ParseError: 615 | print "Error!\nThe given file is not in the XML format!" 616 | ugo = False 617 | if not ugo: 618 | #it prints sufficient errormessages 619 | #print "Error!\n The given file is not a UGO file!" 620 | sys.exit() 621 | print "Done!" 622 | 623 | print "Writing UGO..." 624 | ugo.WriteFile(output) 625 | print "Done" -------------------------------------------------------------------------------- /Hatenatools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/Hatenatools/__init__.py -------------------------------------------------------------------------------- /Hatenatools/hatenatools project page.url: -------------------------------------------------------------------------------- 1 | [{000214A0-0000-0000-C000-000000000046}] 2 | Prop3=19,2 3 | [InternetShortcut] 4 | URL=http://pbsds.net/projects/hatenatools 5 | IDList= 6 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | <one line to give the program's name and a brief idea of what it does.> 633 | Copyright (C) <year> <name of author> 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see <http://www.gnu.org/licenses/>. 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | <http://www.gnu.org/licenses/>. -------------------------------------------------------------------------------- /ReadMe.txt: -------------------------------------------------------------------------------- 1 | == Flipnote Player == 2 | == By pbsds == 3 | == aka Peder Bergebakken Sundt == 4 | 5 | --VERSIONS-- 6 | Flipnote Player v0.9 7 | 8 | --DESCRIPTION-- 9 | Flipnote Player can play the flipnotes from NDSi from local storage and 10 | directly from hatena private servers like sudomemo. Possibly from the real Hatena in the future aswell 11 | 12 | 13 | --DEPENDENCIES-- 14 | Hatenatools: https://github.com/pbsds/Hatenatools, which is included 15 | PIL, pygame, numpy and scikits.samplerate 16 | Under windows: pywin32 17 | Written and tested with python 2.7.3 on Windows 8.1 18 | 19 | --LICENSE-- 20 | Flipnote Player is licensed under AGPL3 21 | See License.txt or http://www.gnu.org/licenses/agpl-3.0.html 22 | -------------------------------------------------------------------------------- /compile.bat: -------------------------------------------------------------------------------- 1 | pyinstaller --icon=icon.ico --onefile -n "Flipnote Player" --noconsole "Flipnote Player.py" 2 | move "dist\Flipnote Player.exe" "Flipnote Player.exe" 3 | rmdir dist 4 | move "build\Flipnote Player\warnFlipnote Player.txt" "warnFlipnote Player.txt" 5 | rmdir /s /q build 6 | del "Flipnote Player.spec" 7 | pause -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [settings] 2 | volume = 1000 3 | scale = True 4 | advancemamescaler = False 5 | 6 | [servers] 7 | count = 1 8 | server1 = Sudofox's Sudomemo:ds.sudomemo.net:80:sudomemo.png 9 | server2 = pbsds' hatena server:hatena.pbsds.net:8080:pbhatena.png 10 | 11 | -------------------------------------------------------------------------------- /graphics/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/bg.png -------------------------------------------------------------------------------- /graphics/browserbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/browserbg.png -------------------------------------------------------------------------------- /graphics/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/button.png -------------------------------------------------------------------------------- /graphics/drive icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/drive icon.png -------------------------------------------------------------------------------- /graphics/folder icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/folder icon.png -------------------------------------------------------------------------------- /graphics/icon base.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/icon base.png -------------------------------------------------------------------------------- /graphics/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/icon.png -------------------------------------------------------------------------------- /graphics/locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/locked.png -------------------------------------------------------------------------------- /graphics/profontwindows.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/profontwindows.ttf -------------------------------------------------------------------------------- /graphics/scrollbarbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/scrollbarbg.png -------------------------------------------------------------------------------- /graphics/scroller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/scroller.png -------------------------------------------------------------------------------- /graphics/servers/pbhatena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/servers/pbhatena.png -------------------------------------------------------------------------------- /graphics/servers/sudomemo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/servers/sudomemo.png -------------------------------------------------------------------------------- /graphics/spinoff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/graphics/spinoff.png -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pbsds/Flipnote-Player/bb9fd67c481ba2ec9d73d4634ddd9ebdf9314375/icon.ico --------------------------------------------------------------------------------