junglejourney
view materials/make_packaging.py @ 185:ae1d814100fa
Removed hard-coded page size.
Formatted text to fit on the page slightly better.
Added the license header text.
| author | David Boddie <david@boddie.org.uk> |
|---|---|
| date | Mon Sep 26 20:13:02 2011 +0200 |
| parents | cd78196f8910 |
| children | 82a73a5987fe |
line source
1 #!/usr/bin/env python
3 """
4 Copyright (C) 2011 David Boddie <david@boddie.org.uk>
6 This program is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program. If not, see <http://www.gnu.org/licenses/>.
18 """
20 import codecs, os, sys
21 from PyQt4.QtCore import QSize
22 from PyQt4.QtGui import *
24 def relpath(source, destination):
26 source = os.path.abspath(source)
27 destination = os.path.abspath(destination)
29 src_pieces = source.split(os.sep)
30 dest_pieces = destination.split(os.sep)
32 if os.path.isfile(source):
33 src_pieces.pop()
35 common = []
36 for i in range(min(len(src_pieces), len(dest_pieces))):
38 if src_pieces[i] == dest_pieces[i]:
39 common.append(src_pieces[i])
40 i -= 1
41 else:
42 break
44 to_common = os.sep.join([os.pardir]*(len(src_pieces)-len(common)))
45 return to_common + os.sep + os.sep.join(dest_pieces[len(common):])
48 class SVG:
50 def __init__(self, path):
52 self.path = path
54 def _escape(self, text):
56 for s, r in (("&", "&"), ("<", "<"), (">", ">")):
57 text = text.replace(s, r)
59 return text
61 def open(self):
63 self.text = ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
64 '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n')
66 def add_page(self, width, height):
68 self.text += (' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n'
69 '<svg version="1.1"\n'
70 ' xmlns="http://www.w3.org/2000/svg"\n'
71 ' xmlns:xlink="http://www.w3.org/1999/xlink"\n'
72 ' width="%fcm" height="%fcm"\n'
73 ' viewBox="0 0 %i %i">\n') % (width/100.0, height/100.0, width, height)
75 def add_image(self, x, y, width, height, path):
77 path = os.path.join(relpath(self.path, os.curdir), path)
78 self.text += '<image x="%f" y="%f" width="%f" height="%f"\n' % (x, y, width, height)
79 self.text += ' xlink:href="%s" />\n' % path
81 def add_text(self, x, y, font, text):
83 self.text += '<text x="%f" y="%f"\n' % (x, y)
84 self.text += (' font-family="%s"\n'
85 ' font-size="%i"\n') % (font["family"], font["size"])
86 if font.has_key("weight"):
87 self.text += ' font-weight="%s"\n' % font["weight"]
88 if font.has_key("style"):
89 self.text += ' font-style="%s"\n' % font["style"]
90 self.text += '>\n'
91 self.text += self._escape(text)
92 self.text += '</text>\n'
94 def close(self):
96 self.text += "</svg>\n"
97 codecs.open(self.path, "w", "utf-8").write(self.text)
100 class Page:
102 def __init__(self, size, objects):
104 self.size = size
105 self.objects = objects
107 def render(self, svg):
109 svg.add_page(self.size[0], self.size[1])
111 positions = [(0, 0)]
112 for obj in self.objects:
114 x, y = obj.render(svg, positions)
115 positions.append((x, y))
117 return svg
119 class TextBox:
121 def __init__(self, bbox, text_items, follow = False, index = -1):
123 self.bbox = bbox
124 self.text_items = text_items
125 self.follow = follow
126 self.index = index
128 def render(self, svg, positions):
130 x, y, width, height = self.bbox
132 if self.follow:
133 y = y + positions[self.index][1]
135 for text_item in self.text_items:
137 left_indent = text_item.font.get("left indent", 0)
138 right_indent = text_item.font.get("right indent", 0)
139 item_x = x + left_indent
140 item_width = width - left_indent - right_indent
142 for pieces, line_height in text_item.readline(item_width):
144 for font, word_x, text in pieces:
146 svg.add_text(item_x + word_x, y, font, text)
148 y += line_height
150 return x, y
152 class Text:
154 def __init__(self, font, text):
156 self.font = font
157 self.text = text
159 self.parse_text()
161 def parse_text(self):
163 lines = self.text.split("\n")
164 self.lines = []
166 for line in lines:
168 words = []
169 for word in line.split():
171 words.append(Word(self.font, word))
173 self.lines.append(words)
175 def readline(self, width):
177 for line in self.lines:
179 w = 0
180 used = 0
181 words = []
183 while w < len(line):
185 word = line[w]
186 word_width = word.width()
188 if used + word_width <= width:
189 # Add words while there is still space.
190 used += word_width + word.space()
191 words.append(word)
192 w += 1
194 elif words:
195 # When out of space, yield the words on the line.
196 yield self.format(words, width), self.height(words)
198 used = 0
199 words = []
201 else:
202 # If no words will fit on the line, just add the first
203 # word to the list.
204 yield self.format([word], width), self.height(words)
206 used = 0
207 w += 1
209 if words:
210 yield self.format(words, width, last = True), self.height(words)
211 elif not line:
212 yield [], self.line_height()/2
214 def format(self, words, width, last = False):
216 output = []
217 x = 0
219 if len(words) == 0:
220 spacing = 0
222 elif self.font.get("align", "left") == "justify" and not last:
223 # Full justify the text.
224 total_width = sum(map(lambda word: word.width(), words))
225 spacing = (width - total_width)/float(len(words) - 1)
227 elif self.font.get("align", "left") == "centre":
228 # Centre the text.
229 total_width = sum(map(lambda word: word.width(), words))
230 total_space = sum(map(lambda word: word.space(), words)[:-1])
231 x = width/2.0 - total_width/2.0 - total_space/2.0
232 spacing = None
234 else:
235 spacing = None
237 for word in words:
239 output.append((word._font, x, word.text))
240 x += word.width()
241 if spacing is not None:
242 x += spacing
243 else:
244 x += word.space()
246 return output
248 def height(self, words):
250 return max(map(lambda word: word.height(), words))
252 def line_height(self):
254 font = QFont(self.font.get("family"))
255 font.setPixelSize(self.font.get("size"))
256 if self.font.get("weight") == "bold":
257 font.setWeight(QFont.Bold)
258 if self.font.get("style") == "italic":
259 font.setItalic(True)
261 metrics = QFontMetrics(font)
262 return metrics.height()
264 class Word:
266 def __init__(self, font, text):
268 self._font = font
269 self.text = text
271 def font(self):
273 font = QFont(self._font.get("family"))
274 font.setPixelSize(self._font.get("size"))
275 if self._font.get("weight") == "bold":
276 font.setWeight(QFont.Bold)
277 if self._font.get("style") == "italic":
278 font.setItalic(True)
279 return font
281 def width(self):
283 metrics = QFontMetrics(self.font())
284 return metrics.width(self.text)
286 def height(self):
288 metrics = QFontMetrics(self.font())
289 return metrics.height()
291 def space(self):
293 metrics = QFontMetrics(self.font())
294 return metrics.width(" ")
297 class Image:
299 def __init__(self, bbox, path, scale = None, follow = False, index = -1):
301 self.bbox = bbox
302 self.path = path
303 self.follow = follow
304 self.index = index
305 self.scale = scale
307 def render(self, svg, positions):
309 x, y, width, height = self.bbox
311 if self.follow:
312 y = y + positions[self.index][1]
314 im = QImage(self.path)
315 width = im.size().width()
316 height = im.size().height()
318 if self.scale:
319 width = width * self.scale
320 height = height * self.scale
322 svg.add_image(x, y, width, height, self.path)
324 return x + width, y + height
327 if __name__ == "__main__":
329 app = QApplication(sys.argv)
331 if len(app.arguments()) != 2:
333 sys.stderr.write("Usage: %s <output directory>\n" % app.arguments()[0])
334 sys.exit(1)
336 output_dir = sys.argv[1]
338 if not os.path.exists(output_dir):
339 os.mkdir(output_dir)
341 regular = {"family": "FreeSerif",
342 "size": 24,
343 "align": "justify"}
345 title = {"family": "FreeSerif",
346 "size": 24,
347 "weight": "bold"}
349 italic_quote = {"family": "FreeSerif",
350 "size": 22,
351 "style": "italic",
352 "left indent": 40,
353 "right indent": 40}
355 quote = {"family": "FreeSerif",
356 "size": 22,
357 "left indent": 40,
358 "right indent": 40}
360 monospace_quote = {"family": "FreeMono",
361 "size": 22,
362 "left indent": 40,
363 "right indent": 40}
365 keys_quote = {"family": "FreeSerif",
366 "size": 24,
367 "left indent": 40,
368 "right indent": 40}
370 key_descriptions_quote = {"family": "FreeSerif",
371 "size": 24,
372 "left indent": 160,
373 "right indent": 0}
375 exclamation = {"family": "FreeSerif",
376 "size": 28,
377 "style": "italic",
378 "weight": "bold",
379 "align": "centre"}
381 back_cover_title = {"family": "FreeSerif",
382 "size": 36,
383 "weight": "bold",
384 "align": "centre"}
386 back_cover_subtitle = {"family": "FreeSerif",
387 "size": 28,
388 "weight": "bold",
389 "align": "centre"}
391 back_cover_centred = {"family": "FreeSerif",
392 "size": 24,
393 "align": "centre"}
395 pages = [
396 Page((650, 1020),
397 [TextBox((25, 35, 600, 0),
398 [Text(title, "Jungle Journey\n"),
399 Text(regular,
400 "The last flames of the campfire fade to glowing embers and I am alone. "
401 "My recent acquaintances, their packs and paraphernalia have gone, leaving "
402 "me stranded deep in the heart of this jungle realm. Clouds momentarily "
403 "sweep the cold face of the moon and I perceive the clicks, whistles and "
404 "cries of creatures in the hot air that cloaks this place. Desperately, I "
405 "try to stay my panic and remember those fragments of wilderness craft "
406 "learned and unlearned many years ago.\n"),
407 Text(italic_quote,
408 "Choose your weapon carefully,\n"
409 "Get ready for a fight.\n"
410 "The jungle can be dangerous\n"
411 "If you go there at night.\n"
412 "There's time to pick up treasure,\n"
413 "But no time to stop and stare.\n"
414 "If you don't find the hidden cave\n"
415 "You won't get out of there.\n"),
416 Text(regular,
417 "Hopeless, I scramble to my feet, reaching for any weapon still left to me. "
418 "Struggling through the dense undergrowth, I search for signs of a track or "
419 "trail. At first glance, paths that seemed to lead to safety turn out to be "
420 "impassable, overgrown by tangled and twisted vines. I remember the words of "
421 "an old teacher:\n"),
422 Text(quote,
423 u'\u201cDo not be tempted to use fire to make your way. '
424 'Many a traveller has strayed from the path, using fire to blaze a trail, '
425 'only to reach a dead end. Trying to return, they find that the jungle '
426 'has grown back. Those who are desperate enough will even seek out '
427 u'forgotten routes when the way home is in sight.\u201d\n'),
428 Text(regular,
429 "Sensing my presence, obscene creatures emerge from the darkness, hungry "
430 "for prey. Only through skill and luck am I able to dispatch them back "
431 "into the shadows. Even though I know I must journey deeper into this "
432 "uncharted land to find the way home, the thought of vengeance drives me on.")
433 ])
434 ]),
435 Page((650, 1000),
436 [TextBox((25, 35, 600, 0),
437 [Text(title, "Loading the Game\n"),
438 Text(regular, "Insert the cassette or disk and type\n")]),
439 TextBox((25, -2, 600, 0),
440 [Text(monospace_quote, "*RUN JUNGLE\n")], follow = True),
441 TextBox((25, -2, 600, 0),
442 [Text(regular,
443 "then press Return. If you are loading the game from cassette, press play on the "
444 "cassette recorder. The game should now load.\n"),
445 Text(title, "Playing the Game\n"),
446 Text(regular,
447 "The player must help the character reach the exit for each level. However, the "
448 "player must first find a key to unlock the exit. On the final level, the exit "
449 "does not require a key but it may be obstructed. Enemies will appear in each "
450 "location and attack the player's character. They can be destroyed by "
451 "projectiles fired by the current weapon.\n"),
452 Text(regular,
453 "Your character can be moved around the screen by using four control keys:\n")],
454 follow = True),
455 TextBox((25, 0, 600, 0),
456 [Text(keys_quote,
457 "Z\n"
458 "X\n"
459 ":\n"
460 "/")], follow = True),
461 TextBox((25, 0, 600, 0),
462 [Text(key_descriptions_quote,
463 "left\n"
464 "right\n"
465 "up\n"
466 "down\n"),
467 Text(regular,
468 "To fire a weapon, press the Return key. There are four different types of "
469 "weapon available in the game.\n\n"
470 "Alternatively, you may may using an analogue joystick connected to a Plus 1 "
471 "expansion interface. Select joystick controls by pressing the J key on the "
472 "title page. Press K to select keyboard controls.\n\n"
473 "Other keys can be used to control the game:\n")],
474 follow = True, index = -2),
475 TextBox((25, 0, 600, 0),
476 [Text(keys_quote,
477 "S\n"
478 "Q\n"
479 "P\n"
480 "O\n"
481 "Escape")], follow = True),
482 TextBox((25, 0, 600, 0),
483 [Text(key_descriptions_quote,
484 "enable sound effects\n"
485 "disable sound effects\n"
486 "pause the game\n"
487 "resume the game\n"
488 "quit the game, returning to the title screen\n")],
489 follow = True, index = -2)
490 ]),
491 Page((650, 1000),
492 [TextBox((25, 35, 600, 0),
493 [Text(title, "Treasure\n"),
494 Text(regular, "Items of treasure can be found throughout the jungle. "
495 "Pick these up to increase your score.\n")]),
496 Image((45, -8, 515, 0), "../images/key.xpm", scale = 4,
497 follow = True),
498 TextBox((135, 20, 475, 0),
499 [Text(regular, "Find the key to open the door on all levels except the last. "
500 "Each key is worth 50 points.")],
501 follow = True, index = -2),
502 Image((45, 8, 515, 0), "../images/chest.xpm", scale = 4,
503 follow = True, index = -2),
504 TextBox((135, 48, 475, 0),
505 [Text(regular, "Treasure chests are worth 20 points.")],
506 follow = True, index = -3),
507 Image((45, 8, 515, 0), "../images/jewel.xpm", scale = 4,
508 follow = True, index = -2),
509 TextBox((135, 48, 475, 0),
510 [Text(regular, "Jewels are worth 5 points.")],
511 follow = True, index = -3),
512 Image((45, 8, 515, 0), "../images/statue.xpm", scale = 4,
513 follow = True, index = -2),
514 TextBox((135, 48, 475, 0),
515 [Text(regular, "Statues are worth 10 points.")],
516 follow = True, index = -3),
517 Image((45, 8, 515, 0), "../images/health.xpm", scale = 4,
518 follow = True, index = -2),
519 TextBox((135, 36, 475, 0),
520 [Text(regular, "Presents are worth 40 points and boost your strength by 20 units.")],
521 follow = True, index = -3),
522 TextBox((25, 48, 600, 0),
523 [Text(title, "Exits\n"),
524 Text(regular, "Each level has an exit that can be opened using a key. "
525 "On the last level you will find a gate that leads to safety. "
526 "This does not require a key, but it is well hidden.\n")],
527 follow = True),
528 Image((77, -4, 513, 0), "../images/exit1.xpm", scale = 4,
529 follow = True),
530 TextBox((215, 36, 400, 0),
531 [Text(regular, "The exit is initially locked. Find the key to unlock it.")],
532 follow = True, index = -2),
533 Image((45, 8, 545, 0), "../images/finalexitl.xpm", scale = 4,
534 follow = True, index = -2),
535 Image((109, 8, 481, 0), "../images/finalexitr.xpm", scale = 4,
536 follow = True, index = -3),
537 TextBox((215, 48, 400, 0),
538 [Text(regular, "The final exit is hidden somewhere on the final level.")],
539 follow = True, index = -4),
540 TextBox((25, 950, 600, 0),
541 [Text(exclamation, "Have a safe journey!")])
542 ]),
543 Page((650, 1000),
544 [TextBox((25, 50, 600, 0),
545 [Text(back_cover_title, "Jungle Journey"),
546 Text(back_cover_subtitle, "for the Acorn Electron and BBC Model B")]),
547 Image((101, 0, 450, 0), "screenshot1.png", scale = 0.7, follow = True),
548 TextBox((25, 45, 600, 0),
549 [Text(back_cover_centred,
550 u"Copyright \u00a9 2011 David Boddie\n"
551 u"An Infukor production for Retro Software\n"
552 u"http://www.retrosoftware.co.uk/")], follow = True),
553 TextBox((25, 20, 600, 0),
554 [Text(regular,
555 "This program is free software: you can redistribute it and/or modify "
556 "it under the terms of the GNU General Public License as published by "
557 "the Free Software Foundation, either version 3 of the License, or "
558 "(at your option) any later version.\n"
559 "\n"
560 "This program is distributed in the hope that it will be useful, "
561 "but WITHOUT ANY WARRANTY; without even the implied warranty of "
562 "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the "
563 "GNU General Public License for more details.\n"
564 "\n"
565 "You should have received a copy of the GNU General Public License "
566 "along with this program.\nIf not, see <http://www.gnu.org/licenses/>.")],
567 follow = True)
568 ]),
569 ]
571 i = 0
572 for page in pages:
574 path = os.path.join(output_dir, "page-%i.svg" % i)
575 svg = SVG(path)
576 svg.open()
577 page.render(svg)
578 svg.close()
579 i += 1
581 sys.exit()
